diff --git a/analyzer.go b/analyzer.go index edadbd2..a2b11e7 100644 --- a/analyzer.go +++ b/analyzer.go @@ -3,10 +3,10 @@ package ginkgolinter import ( "flag" "fmt" - "github.com/nunnatsa/ginkgolinter/gomegaanalyzer" "golang.org/x/tools/go/analysis" + "github.com/nunnatsa/ginkgolinter/gomegaanalyzer" "github.com/nunnatsa/ginkgolinter/linter" "github.com/nunnatsa/ginkgolinter/types" "github.com/nunnatsa/ginkgolinter/version" @@ -28,14 +28,15 @@ 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, - ForceSucceedForFuncs: false, + SuppressLen: false, + SuppressNil: false, + SuppressErr: false, + SuppressCompare: false, + ForbidFocus: false, + AllowHaveLen0: false, + ForceExpectTo: false, + ForceSucceedForFuncs: false, + ForbidAsyncGlobalAssertions: false, } a := NewAnalyzerWithConfig(config) @@ -53,6 +54,7 @@ func NewAnalyzer() *analysis.Analyzer { a.Flags.BoolVar(&config.ForbidFocus, "forbid-focus-container", config.ForbidFocus, "trigger a warning for ginkgo focus containers like FDescribe, FContext, FWhen or FIt; default = false.") a.Flags.BoolVar(&config.ForbidSpecPollution, "forbid-spec-pollution", config.ForbidSpecPollution, "trigger a warning for variable assignments in ginkgo containers like Describe, Context and When, instead of in BeforeEach(); default = false.") a.Flags.BoolVar(&config.ForceSucceedForFuncs, "force-succeed", config.ForceSucceedForFuncs, "force using the Succeed matcher for error functions, and the HaveOccurred matcher for non-function error values") + a.Flags.BoolVar(&config.ForbidAsyncGlobalAssertions, "forbid-global-assertion", config.ForbidAsyncGlobalAssertions, "don't allow global Expect inside async assertions") return a } diff --git a/analyzer_test.go b/analyzer_test.go index ed82d1f..907b08e 100644 --- a/analyzer_test.go +++ b/analyzer_test.go @@ -192,6 +192,13 @@ func TestFlags(t *testing.T) { "force-succeed": "true", }, }, + { + testName: "forbid global async assertions", + testData: []string{"a/asyncglobal"}, + flags: map[string]string{ + "forbid-global-assertion": "true", + }, + }, } { t.Run(tc.testName, func(tt *testing.T) { analyzer := ginkgolinter.NewAnalyzer() diff --git a/gomegaanalyzer/analyzer.go b/gomegaanalyzer/analyzer.go index 31392ac..444b152 100644 --- a/gomegaanalyzer/analyzer.go +++ b/gomegaanalyzer/analyzer.go @@ -1,34 +1,71 @@ package gomegaanalyzer import ( - "github.com/nunnatsa/ginkgolinter/internal/expression" - "github.com/nunnatsa/ginkgolinter/internal/gomegahandler" "go/ast" - "golang.org/x/tools/go/analysis" + "go/token" "reflect" + "sort" + + "github.com/nunnatsa/ginkgolinter/internal/asyncfuncpos" + + "golang.org/x/tools/go/analysis" + + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/gomegahandler" ) -type Result map[ast.Expr]*expression.GomegaExpression +type FileResult map[ast.Expr]*expression.GomegaExpression + +type Result struct { + expressions map[*ast.File]FileResult + asyncScope asyncfuncpos.Poses +} + +func (r Result) GetFileResults() map[*ast.File]FileResult { + return r.expressions +} + +func (r Result) GetFileResult(file *ast.File) (FileResult, bool) { + fr, ok := r.expressions[file] + return fr, ok +} + +func (r Result) InAsyncScope(pos token.Pos) (token.Pos, bool) { + if fncPos, found := r.asyncScope.Contains(pos); found { + return fncPos.CallPos, true + } + + return token.NoPos, false +} func New() *analysis.Analyzer { return &analysis.Analyzer{ Name: "gomegaAnalyzer", Doc: "gather all gomega expressions", Run: run, - ResultType: reflect.TypeOf(Result{}), + ResultType: reflect.TypeOf(&Result{}), } } func run(pass *analysis.Pass) (any, error) { - expressions := make(Result) + expressions := make(map[*ast.File]FileResult) + var funcPoss asyncfuncpos.Poses + for _, file := range pass.Files { gomegaHndlr := gomegahandler.GetGomegaHandler(file, pass) if gomegaHndlr == nil { continue } + fileResult := make(FileResult) + ast.Inspect(file, func(n ast.Node) bool { - call, ok := n.(*ast.CallExpr) + stmt, ok := n.(*ast.ExprStmt) + if !ok { + return true + } + + call, ok := stmt.X.(*ast.CallExpr) if !ok { return true } @@ -38,13 +75,23 @@ func run(pass *analysis.Pass) (any, error) { return true } - expressions[call] = expr + fileResult[call] = expr + + if expr.IsAsync() { + if arg := expr.GetAsyncActualArg(); arg != nil && arg.HasAsyncFuncPos() { + funcPoss = append(funcPoss, arg.AsyncFucPos()) + } + } return true }) + + expressions[file] = fileResult } - return expressions, nil + sort.Sort(funcPoss) + + return &Result{expressions: expressions, asyncScope: funcPoss}, nil } func getTimePkg(file *ast.File) string { diff --git a/internal/asyncfuncpos/asyncfuncpos.go b/internal/asyncfuncpos/asyncfuncpos.go new file mode 100644 index 0000000..d3b6997 --- /dev/null +++ b/internal/asyncfuncpos/asyncfuncpos.go @@ -0,0 +1,48 @@ +package asyncfuncpos + +import ( + "go/token" + gotypes "go/types" +) + +type AsyncFuncPos struct { + Scope *gotypes.Scope + CallPos token.Pos +} + +type Poses []*AsyncFuncPos + +func (p Poses) Len() int { + return len(p) +} + +func (p Poses) Less(i, j int) bool { + return p[i].Scope.Pos() < p[j].Scope.Pos() +} + +func (p Poses) Swap(i, j int) { + p[i], p[j] = p[j], p[i] +} + +func (p Poses) Contains(pos token.Pos) (*AsyncFuncPos, bool) { + if len(p) == 0 { + return nil, false + } + mid := len(p) / 2 + + if p[mid].Scope.Contains(pos) { + return p[0], true + } + + left := p[:mid] + if l := len(left); l > 0 && left[0].Scope.Pos() <= pos && left[l-1].Scope.End() >= pos { + return left.Contains(pos) + } else { + right := p[mid+1:] + if l := len(right); l > 0 && right[0].Scope.Pos() <= pos && right[l-1].Scope.End() >= pos { + return right.Contains(pos) + } + } + + return nil, false +} diff --git a/internal/expression/actual/asyncactual.go b/internal/expression/actual/asyncactual.go index 7c5df2a..6d9afe4 100644 --- a/internal/expression/actual/asyncactual.go +++ b/internal/expression/actual/asyncactual.go @@ -6,6 +6,7 @@ import ( "golang.org/x/tools/go/analysis" + "github.com/nunnatsa/ginkgolinter/internal/asyncfuncpos" "github.com/nunnatsa/ginkgolinter/internal/intervals" ) @@ -17,6 +18,7 @@ type AsyncArg struct { pollingInterval intervals.DurationValue tooManyTimeouts bool tooManyPolling bool + asyncPos *asyncfuncpos.AsyncFuncPos } func newAsyncArg(origExpr, cloneExpr, orig, clone *ast.CallExpr, argType gotypes.Type, pass *analysis.Pass, actualOffset int, timePkg string) *AsyncArg { @@ -25,15 +27,52 @@ func newAsyncArg(origExpr, cloneExpr, orig, clone *ast.CallExpr, argType gotypes valid = true timeout intervals.DurationValue polling intervals.DurationValue + funcPos *asyncfuncpos.AsyncFuncPos ) - if _, isActualFuncCall := orig.Args[actualOffset].(*ast.CallExpr); isActualFuncCall { + switch arg := orig.Args[actualOffset].(type) { + case *ast.CallExpr: fun = clone.Args[actualOffset].(*ast.CallExpr) valid = isValidAsyncValueType(argType) + + case *ast.FuncLit: + var scope *gotypes.Scope + s, ok := pass.TypesInfo.Scopes[arg] + if ok { + scope = s + } else { + scope = gotypes.NewScope(nil, arg.Pos(), arg.End(), "") + } + + funcPos = &asyncfuncpos.AsyncFuncPos{ + Scope: scope, + CallPos: arg.Pos(), + } + + case *ast.Ident: + if _, ok := pass.TypesInfo.TypeOf(arg).(*gotypes.Signature); ok { + if s, ok := pass.TypesInfo.Uses[arg]; ok { + if theFunc, ok := s.(*gotypes.Func); ok { + funcPos = &asyncfuncpos.AsyncFuncPos{ + Scope: theFunc.Scope(), + CallPos: arg.Pos(), + } + } + } + } + + case *ast.SelectorExpr: + if sel, ok := pass.TypesInfo.Uses[arg.Sel]; ok { + if theFunc, ok := sel.(*gotypes.Func); ok { + funcPos = &asyncfuncpos.AsyncFuncPos{ + Scope: theFunc.Scope(), + CallPos: arg.Pos(), + } + } + } } timeoutOffset := actualOffset + 1 - //var err error tooManyTimeouts := false tooManyPolling := false @@ -87,6 +126,7 @@ func newAsyncArg(origExpr, cloneExpr, orig, clone *ast.CallExpr, argType gotypes pollingInterval: polling, tooManyTimeouts: tooManyTimeouts, tooManyPolling: tooManyPolling, + asyncPos: funcPos, } } @@ -110,6 +150,14 @@ func (a *AsyncArg) TooManyPolling() bool { return a.tooManyPolling } +func (a *AsyncArg) HasAsyncFuncPos() bool { + return a.asyncPos != nil +} + +func (a *AsyncArg) AsyncFucPos() *asyncfuncpos.AsyncFuncPos { + return a.asyncPos +} + func isValidAsyncValueType(t gotypes.Type) bool { switch t.(type) { // allow functions that return function or channel. diff --git a/internal/expression/expression.go b/internal/expression/expression.go index 6e8e0db..21bd72f 100644 --- a/internal/expression/expression.go +++ b/internal/expression/expression.go @@ -22,6 +22,7 @@ import ( type GomegaExpression struct { orig *ast.CallExpr clone *ast.CallExpr + pos token.Pos assertionFuncName string origAssertionFuncName string @@ -85,6 +86,7 @@ func New(origExpr *ast.CallExpr, pass *analysis.Pass, handler gomegahandler.Hand gexp := &GomegaExpression{ orig: origExpr, clone: exprClone, + pos: origExpr.Pos(), assertionFuncName: origSel.Sel.Name, origAssertionFuncName: origSel.Sel.Name, @@ -106,6 +108,10 @@ func New(origExpr *ast.CallExpr, pass *analysis.Pass, handler gomegahandler.Hand return gexp, true } +func (e *GomegaExpression) Pos() token.Pos { + return e.pos +} + func (e *GomegaExpression) IsMissingAssertion() bool { return e.matcher == nil } diff --git a/internal/formatter/formatter.go b/internal/formatter/formatter.go index 64f3d99..c7529c0 100644 --- a/internal/formatter/formatter.go +++ b/internal/formatter/formatter.go @@ -20,3 +20,7 @@ func (f GoFmtFormatter) Format(exp ast.Expr) string { _ = printer.Fprint(&buf, f.fset, exp) return buf.String() } + +func (f GoFmtFormatter) FormatPosition(pos token.Pos) string { + return f.fset.Position(pos).String() +} diff --git a/internal/reports/report-builder.go b/internal/reports/report-builder.go index dee88bd..105beda 100644 --- a/internal/reports/report-builder.go +++ b/internal/reports/report-builder.go @@ -38,6 +38,11 @@ func (b *Builder) OldExp() string { } func (b *Builder) AddIssue(suggestFix bool, issue string, args ...any) { + for i, arg := range args { + if p, ok := arg.(token.Pos); ok { + args[i] = b.formatter.FormatPosition(p) + } + } if len(args) > 0 { issue = fmt.Sprintf(issue, args...) } diff --git a/internal/rules/asyncglobalexpect.go b/internal/rules/asyncglobalexpect.go new file mode 100644 index 0000000..c44e14b --- /dev/null +++ b/internal/rules/asyncglobalexpect.go @@ -0,0 +1,28 @@ +package rules + +import ( + "github.com/nunnatsa/ginkgolinter/gomegaanalyzer" + "github.com/nunnatsa/ginkgolinter/internal/expression" + "github.com/nunnatsa/ginkgolinter/internal/reports" + "github.com/nunnatsa/ginkgolinter/types" +) + +type AsyncGlobalExpect struct { + res *gomegaanalyzer.Result +} + +func NewAsyncGlobalExpect(res *gomegaanalyzer.Result) *AsyncGlobalExpect { + return &AsyncGlobalExpect{res: res} +} + +func (r AsyncGlobalExpect) Apply(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder) bool { + if !bool(config.ForbidAsyncGlobalAssertions) || gexp.IsAsync() || gexp.IsUsingGomegaVar() { + return false + } + + if pos, inScope := r.res.InAsyncScope(gexp.Pos()); inScope { + reportBuilder.AddIssue(false, "using a global %s in an async assertion at %v; either use a Gomega variable, or StopTrying(...).Now()", gexp.GetActualFuncName(), pos) + } + + return false +} diff --git a/internal/rules/rule.go b/internal/rules/rule.go index cf331c2..ba8f2bf 100644 --- a/internal/rules/rule.go +++ b/internal/rules/rule.go @@ -1,6 +1,7 @@ package rules import ( + "github.com/nunnatsa/ginkgolinter/gomegaanalyzer" "github.com/nunnatsa/ginkgolinter/internal/expression" "github.com/nunnatsa/ginkgolinter/internal/reports" "github.com/nunnatsa/ginkgolinter/types" @@ -10,19 +11,26 @@ 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 rules = Rules{} + +func GetRules(gomegaRes *gomegaanalyzer.Result) Rules { + rules = Rules{ + NewAsyncGlobalExpect(gomegaRes), + &ForceExpectToRule{}, + &LenRule{}, + &CapRule{}, + &ComparisonRule{}, + &NilCompareRule{}, + &ComparePointRule{}, + &ErrorEqualNilRule{}, + &MatchErrorRule{}, + getMatcherOnlyRules(), + &EqualDifferentTypesRule{}, + &HaveOccurredRule{}, + &SucceedRule{}, + } + + return rules } var asyncRules = Rules{ @@ -34,10 +42,6 @@ var asyncRules = Rules{ getMatcherOnlyRules(), } -func GetRules() Rules { - return rules -} - func GetAsyncRules() Rules { return asyncRules } diff --git a/linter/ginkgo_linter.go b/linter/ginkgo_linter.go index 4693800..291df93 100644 --- a/linter/ginkgo_linter.go +++ b/linter/ginkgo_linter.go @@ -1,9 +1,10 @@ package linter import ( - "github.com/nunnatsa/ginkgolinter/gomegaanalyzer" "go/ast" + "github.com/nunnatsa/ginkgolinter/gomegaanalyzer" + "golang.org/x/tools/go/analysis" "github.com/nunnatsa/ginkgolinter/internal/expression" @@ -34,9 +35,15 @@ func NewGinkgoLinter(config *types.Config, gomegaAnalyzer *analysis.Analyzer) *G // Run is the main assertion function func (l *GinkgoLinter) Run(pass *analysis.Pass) (any, error) { - gomegaAnalyzerRes := pass.ResultOf[l.gomegaAnalyzer].(gomegaanalyzer.Result) + gomegaAnalyzerRes := pass.ResultOf[l.gomegaAnalyzer].(*gomegaanalyzer.Result) + expRules := rules.GetRules(gomegaAnalyzerRes) for _, file := range pass.Files { + gomegaResults, ok := gomegaAnalyzerRes.GetFileResult(file) + if !ok { + continue + } + fileConfig := l.config.Clone() cm := ast.NewCommentMap(pass.Fset, file, file.Comments) @@ -91,20 +98,21 @@ func (l *GinkgoLinter) Run(pass *analysis.Pass) (any, error) { return true } - //gexp, ok := expression.New(assertionExp, pass, gomegaHndlr, getTimePkg(file)) - gexp, ok := gomegaAnalyzerRes[assertionExp] + //gexp, ok := expression.New(assertionExp, pass, gomegaHndlr, "time") + + gexp, ok := gomegaResults[assertionExp] if !ok || gexp == nil { return true } reportBuilder := reports.NewBuilder(assertionExp, formatter.NewGoFmtFormatter(pass.Fset)) - return checkGomegaExpression(gexp, config, reportBuilder, pass) + return checkGomegaExpression(gexp, config, reportBuilder, pass, expRules) }) } return nil, nil } -func checkGomegaExpression(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder, pass *analysis.Pass) bool { +func checkGomegaExpression(gexp *expression.GomegaExpression, config types.Config, reportBuilder *reports.Builder, pass *analysis.Pass, expRules rules.Rules) bool { goNested := false if rules.GetMissingAssertionRule().Apply(gexp, config, reportBuilder) { goNested = true @@ -113,7 +121,7 @@ func checkGomegaExpression(gexp *expression.GomegaExpression, config types.Confi rules.GetAsyncRules().Apply(gexp, config, reportBuilder) goNested = true } else { - rules.GetRules().Apply(gexp, config, reportBuilder) + expRules.Apply(gexp, config, reportBuilder) } } @@ -124,14 +132,3 @@ func checkGomegaExpression(gexp *expression.GomegaExpression, config types.Confi return goNested } - -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 - } - } - - return timePkg -} diff --git a/testdata/src/a/asyncglobal/asyncglobal.go b/testdata/src/a/asyncglobal/asyncglobal.go new file mode 100644 index 0000000..b6d5566 --- /dev/null +++ b/testdata/src/a/asyncglobal/asyncglobal.go @@ -0,0 +1,58 @@ +package asyncglobal + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Checkginkgo", func() { + + It("", func() { + Eventually(func(g Gomega) { + Expect(42).To(Equal(nil)) // want `multiple issues:.*using a global Expect in an async assertion at.*; wrong nil assertion. Consider using .Expect\(42\)\.To\(BeNil\(\)\). instead` + g.Expect(len("abcd")).To(Equal(4)) // want `wrong length assertion` + }).Should(Succeed()) + }) + + It("", func() { + Eventually(func(g Gomega) { + Ω(42).To(Equal(42)) // want `using a global Ω in an async assertion at` + Expect(42).To(Equal(42)) // want `using a global Expect in an async assertion at` + g.Expect(42).To(Equal(42)) + }).Should(Succeed()) + }) + + It("", func(ctx context.Context) { + i := 42 + Eventually(func(ctx context.Context) *int { + var err error + Expect(err).ToNot(HaveOccurred()) // want `using a global Expect in an async assertion` + return &i + }).WithTimeout(60 * time.Second).WithPolling(time.Second).WithContext(ctx).Should(HaveValue(Equal(42))) + + Eventually(func(g Gomega) { + Expect(42).To(Equal(42)) // want `using a global Expect in an async assertion` + g.Expect(42).To(Equal(42)) + }).Should(Succeed()) + }) + + It("", func() { + Eventually(helperFuncFromAsync).Should(Succeed()) + }) + + It("should not fail; no async", func() { + helperFuncFromIt() + }) +}) + +func helperFuncFromAsync(g Gomega) { + Expect(42).To(Equal(42)) // want `using a global Expect in an async assertion` + g.Expect(42).To(Equal(42)) +} + +func helperFuncFromIt() { + Expect(42).To(Equal(42)) +} diff --git a/testdata/src/a/asyncglobal/asyncglobal.name.go b/testdata/src/a/asyncglobal/asyncglobal.name.go new file mode 100644 index 0000000..50903c9 --- /dev/null +++ b/testdata/src/a/asyncglobal/asyncglobal.name.go @@ -0,0 +1,45 @@ +package asyncglobal + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +var _ = Describe("Checkginkgo", func() { + + It("", func() { + gomega.Eventually(func(g gomega.Gomega) { + gomega.Expect(42).To(gomega.Equal(nil)) // want `multiple issues: using a global Expect in an async assertion at.*; wrong nil assertion. Consider using .gomega\.Expect\(42\)\.To\(gomega\.BeNil\(\)\). instead` + g.Expect(len("abcd")).To(gomega.Equal(4)) // want `wrong length assertion` + }).Should(gomega.Succeed()) + }) + + It("", func() { + gomega.Eventually(func(g gomega.Gomega) { + gomega.Ω(42).To(gomega.Equal(42)) // want `using a global Ω in an async assertion at` + gomega.Expect(42).To(gomega.Equal(42)) // want `using a global Expect in an async assertion at` + g.Expect(42).To(gomega.Equal(42)) + }).Should(gomega.Succeed()) + }) + + It("", func(ctx context.Context) { + i := 42 + gomega.Eventually(func(ctx context.Context) *int { + var err error + gomega.Expect(err).ToNot(gomega.HaveOccurred()) // want `using a global Expect in an async assertion at` + return &i + }).WithTimeout(60 * time.Second).WithPolling(time.Second).WithContext(ctx).Should(gomega.HaveValue(gomega.Equal(42))) + + gomega.Eventually(func(g gomega.Gomega) { + gomega.Expect(42).To(gomega.Equal(42)) // want `using a global Expect in an async assertion at` + g.Expect(42).To(gomega.Equal(42)) + }).Should(gomega.Succeed()) + }) + + It("", func() { + gomega.Eventually(helperFuncFromAsync).Should(gomega.Succeed()) + }) +}) diff --git a/testdata/src/a/noassersion/ginkgo.go b/testdata/src/a/noassersion/ginkgo.go index 20c71ef..27802d1 100644 --- a/testdata/src/a/noassersion/ginkgo.go +++ b/testdata/src/a/noassersion/ginkgo.go @@ -13,8 +13,10 @@ var _ = Describe("", func() { Eventually(func() {}, 100*time.Millisecond, 10*time.Millisecond) // want `ginkgo-linter: "Eventually": missing assertion method\. Expected "Should\(\)" or "ShouldNot\(\)"` Eventually(func() {}).Within(100 * time.Millisecond).WithPolling(10 * time.Millisecond) // want `ginkgo-linter: "Eventually": missing assertion method\. Expected "Should\(\)" or "ShouldNot\(\)"` Consistently(func() bool { return true }) // want `ginkgo-linter: "Consistently": missing assertion method\. Expected "Should\(\)" or "ShouldNot\(\)"` - EventuallyWithOffset(1, func(g Gomega) { g.Expect(true) }).WithTimeout(2 * time.Second) // want `ginkgo-linter: "EventuallyWithOffset": missing assertion method. Expected "Should\(\)" or "ShouldNot\(\)"` `ginkgo-linter: "Expect": missing assertion method. Expected "To\(\)", "ToNot\(\)" or "NotTo\(\)"` - ConsistentlyWithOffset(2, func() bool { return true }) // want `ginkgo-linter: "ConsistentlyWithOffset": missing assertion method\. Expected "Should\(\)" or "ShouldNot\(\)"` - Ω("omega") // want `ginkgo-linter: "Ω": missing assertion method\. Expected "Should\(\)", "To\(\)", "ShouldNot\(\)", "ToNot\(\)" or "NotTo\(\)"` + EventuallyWithOffset(1, func(g Gomega) { // want `ginkgo-linter: "EventuallyWithOffset": missing assertion method. Expected "Should\(\)" or "ShouldNot\(\)"` + g.Expect(true) // want `ginkgo-linter: "Expect": missing assertion method. Expected "To\(\)", "ToNot\(\)" or "NotTo\(\)"` + }).WithTimeout(2 * time.Second) + ConsistentlyWithOffset(2, func() bool { return true }) // want `ginkgo-linter: "ConsistentlyWithOffset": missing assertion method\. Expected "Should\(\)" or "ShouldNot\(\)"` + Ω("omega") // want `ginkgo-linter: "Ω": missing assertion method\. Expected "Should\(\)", "To\(\)", "ShouldNot\(\)", "ToNot\(\)" or "NotTo\(\)"` }) }) diff --git a/tests/testdata/asyncglobal.txtar b/tests/testdata/asyncglobal.txtar new file mode 100644 index 0000000..fb1e5b1 --- /dev/null +++ b/tests/testdata/asyncglobal.txtar @@ -0,0 +1,97 @@ +# run ginkgolinter to global async assertions +exec ginkgolinter asyncglobal + +# enable the rule +! exec ginkgolinter --forbid-global-assertion asyncglobal +! stdout . +stderr -count=1 'using a global Ω in an async assertion at' +stderr -count=1 'using a global Expect in an async assertion at' +stderr -count=1 'using a global ExpectWithOffset in an async assertion at' + +-- pointers.go -- +package asyncglobal + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("global async assertions", func() { + It("find global async assertions", func() { + Eventually(func(g Gomega) { + Ω(42).To(Equal(42)) + Expect(42).To(Equal(42)) + g.Expect(42).To(Equal(42)) + }).Should(Succeed()) + }) + + It("find in helper func", func() { + Eventually(f1).Should(Succeed()) + }) +}) + +func f1(g Gomega) { + ExpectWithOffset(1, 42).To(Equal(42)) + g.Expect(42).To(Equal(42)) +} + +-- go.mod -- +module asyncglobal + +go 1.22 + +require ( + github.com/onsi/ginkgo/v2 v2.13.2 + github.com/onsi/gomega v1.30.0 +) + +require ( + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.16.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +-- go.sum -- +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU= +github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= +github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/types/config.go b/types/config.go index 81a9ebe..5f47420 100644 --- a/types/config.go +++ b/types/config.go @@ -17,18 +17,19 @@ const ( ) type Config struct { - SuppressLen bool - SuppressNil bool - SuppressErr bool - SuppressCompare bool - SuppressAsync bool - ForbidFocus bool - SuppressTypeCompare bool - AllowHaveLen0 bool - ForceExpectTo bool - ValidateAsyncIntervals bool - ForbidSpecPollution bool - ForceSucceedForFuncs bool + SuppressLen bool + SuppressNil bool + SuppressErr bool + SuppressCompare bool + SuppressAsync bool + ForbidFocus bool + SuppressTypeCompare bool + AllowHaveLen0 bool + ForceExpectTo bool + ValidateAsyncIntervals bool + ForbidSpecPollution bool + ForceSucceedForFuncs bool + ForbidAsyncGlobalAssertions bool } func (s *Config) AllTrue() bool { @@ -37,18 +38,19 @@ func (s *Config) AllTrue() bool { func (s *Config) Clone() Config { return Config{ - SuppressLen: s.SuppressLen, - SuppressNil: s.SuppressNil, - SuppressErr: s.SuppressErr, - SuppressCompare: s.SuppressCompare, - SuppressAsync: s.SuppressAsync, - ForbidFocus: s.ForbidFocus, - SuppressTypeCompare: s.SuppressTypeCompare, - AllowHaveLen0: s.AllowHaveLen0, - ForceExpectTo: s.ForceExpectTo, - ValidateAsyncIntervals: s.ValidateAsyncIntervals, - ForbidSpecPollution: s.ForbidSpecPollution, - ForceSucceedForFuncs: s.ForceSucceedForFuncs, + SuppressLen: s.SuppressLen, + SuppressNil: s.SuppressNil, + SuppressErr: s.SuppressErr, + SuppressCompare: s.SuppressCompare, + SuppressAsync: s.SuppressAsync, + ForbidFocus: s.ForbidFocus, + SuppressTypeCompare: s.SuppressTypeCompare, + AllowHaveLen0: s.AllowHaveLen0, + ForceExpectTo: s.ForceExpectTo, + ValidateAsyncIntervals: s.ValidateAsyncIntervals, + ForbidSpecPollution: s.ForbidSpecPollution, + ForceSucceedForFuncs: s.ForceSucceedForFuncs, + ForbidAsyncGlobalAssertions: s.ForbidAsyncGlobalAssertions, } }