From 5850de31cb583487c7419472c865f406584f82b7 Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Mon, 16 Mar 2026 07:43:52 +0100 Subject: [PATCH] Add `wrong-t` checker for incorrect testing.T usage in testify assertions Signed-off-by: Matthieu MOREL --- .golangci.yml | 3 + README.md | 53 +++ analyzer/checkers_factory_test.go | 2 + .../checkers-default/wrong-t/wrong_t_test.go | 131 ++++++ internal/checkers/checkers_registry.go | 1 + internal/checkers/checkers_registry_test.go | 2 + internal/checkers/wrong_t.go | 410 ++++++++++++++++++ internal/testgen/gen_wrong_t.go | 177 ++++++++ internal/testgen/main.go | 1 + 9 files changed, 780 insertions(+) create mode 100644 analyzer/testdata/src/checkers-default/wrong-t/wrong_t_test.go create mode 100644 internal/checkers/wrong_t.go create mode 100644 internal/testgen/gen_wrong_t.go diff --git a/.golangci.yml b/.golangci.yml index 3263ab3b..b21f6814 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -112,6 +112,9 @@ linters: - pkg: golang.org/x/tools/go/analysis desc: Please, implement helpers without usage of x/tools + exhaustive: + default-signifies-exhaustive: true + forbidigo: forbid: - pattern: panic diff --git a/README.md b/README.md index 3aac66ea..79fc434d 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ https://golangci-lint.run/docs/linters/configuration/#testifylint | [suite-subtest-run](#suite-subtest-run) | ✅ | ❌ | | [suite-thelper](#suite-thelper) | ❌ | ✅ | | [useless-assert](#useless-assert) | ✅ | ❌ | +| [wrong-t](#wrong-t) | ✅ | ❌ | > ⚠️ Also look at open for contribution [checkers](CONTRIBUTING.md#open-for-contribution) @@ -1240,6 +1241,58 @@ assert.LessOrEqual(0, uintVal) --- +### wrong-t + +```go +❌ +func TestFoo(t *testing.T) { + assert.Equal(nil, expected, actual) + + u := &testing.T{} + assert.Equal(u, expected, actual) +} + +func TestBar(t *testing.T) { + a := assert.New(t) + + t.Run("subtest", func(t *testing.T) { + a.Equal(expected, actual) // a was created with the outer t + }) +} + +✅ +func TestFoo(t *testing.T) { + assert.Equal(t, expected, actual) +} + +func TestBar(t *testing.T) { + t.Run("subtest", func(t *testing.T) { + a := assert.New(t) + a.Equal(expected, actual) + }) +} +``` + +**Autofix**: false.
+**Enabled by default**: true.
+**Reason**: Protection from bugs and misleading test output. + +The checker handles two related cases of incorrect `testing.T` usage: + +#### 1) Nil or freshly created `testing.T` in package-level assertions + +Passing `nil` or a freshly allocated `&testing.T{}` / `new(testing.T)` as the `t` argument will +cause a panic at runtime (nil dereference) or silently misattribute test failures. + +#### 2) Assertion object created with outer `t` used in a subtest + +When `assert.New(t)` or `require.New(t)` is called in an outer scope, the resulting +`*Assertions` object is bound to the outer `testing.T`. Using it inside a `t.Run` subtest +(which receives its own fresh `t`) causes test failures to be reported against the parent +test rather than the subtest, breaking the display of test names in failure output. + +--- + ## Chain of warnings Linter does not automatically handle the "evolution" of changes. diff --git a/analyzer/checkers_factory_test.go b/analyzer/checkers_factory_test.go index ff5512a7..12103c45 100644 --- a/analyzer/checkers_factory_test.go +++ b/analyzer/checkers_factory_test.go @@ -62,6 +62,7 @@ func Test_newCheckers(t *testing.T) { checkers.NewSuiteBrokenParallel(), checkers.NewSuiteMethodSignature(), checkers.NewSuiteSubtestRun(), + checkers.NewWrongT(), } allAdvancedCheckers := []checkers.AdvancedChecker{ checkers.NewBlankImport(), @@ -71,6 +72,7 @@ func Test_newCheckers(t *testing.T) { checkers.NewSuiteMethodSignature(), checkers.NewSuiteSubtestRun(), checkers.NewSuiteTHelper(), + checkers.NewWrongT(), } formatterWithoutEnabledOptions := checkers.RegularChecker(checkers.NewFormatter(). diff --git a/analyzer/testdata/src/checkers-default/wrong-t/wrong_t_test.go b/analyzer/testdata/src/checkers-default/wrong-t/wrong_t_test.go new file mode 100644 index 00000000..0f57bd6f --- /dev/null +++ b/analyzer/testdata/src/checkers-default/wrong-t/wrong_t_test.go @@ -0,0 +1,131 @@ +// Code generated by testifylint/internal/testgen. DO NOT EDIT. + +package wrongt + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Case 1: nil testing.T passed to package-level assertions. +func TestWrongTChecker_NilT(t *testing.T) { + var err error + var result int + + assert.Equal(nil, 0, result) // want "wrong-t: do not pass nil as testing\\.T" + assert.NoError(nil, err) // want "wrong-t: do not pass nil as testing\\.T" + require.Equal(nil, 0, result) // want "wrong-t: do not pass nil as testing\\.T" + require.NoError(nil, err) // want "wrong-t: do not pass nil as testing\\.T" + + // Valid – using the test's t. + assert.Equal(t, 0, result) + assert.NoError(t, err) + require.Equal(t, 0, result) + require.NoError(t, err) + + t.Run("sub", func(t *testing.T) { + assert.Equal(t, 0, result) + require.NoError(t, err) + }) +} + +// Case 2: freshly created testing.T passed to package-level assertions. +func TestWrongTChecker_FreshT(t *testing.T) { + var err error + var result int + + assert.Equal(&testing.T{}, 0, result) // want "wrong-t: do not pass freshly created testing\\.T" + assert.NoError(new(testing.T), err) // want "wrong-t: do not pass freshly created testing\\.T" + require.Equal(&testing.T{}, 0, result) // want "wrong-t: do not pass freshly created testing\\.T" + require.NoError(new(testing.T), err) // want "wrong-t: do not pass freshly created testing\\.T" + + u := &testing.T{} + assert.Equal(u, 0, result) // want "wrong-t: do not pass freshly created testing\\.T" + require.Equal(u, 0, result) // want "wrong-t: do not pass freshly created testing\\.T" + + v := new(testing.T) + assert.NoError(v, err) // want "wrong-t: do not pass freshly created testing\\.T" + require.NoError(v, err) // want "wrong-t: do not pass freshly created testing\\.T" + + // Valid – fresh variable reassigned to a real t before use. + w := &testing.T{} + w = t + assert.Equal(w, 0, result) + require.NoError(w, err) +} + +// Case 3: assertion object created with outer t used inside t.Run subtest. +func TestWrongTChecker_Scope(t *testing.T) { + a := assert.New(t) + r := require.New(t) + + var err error + var result int + + // Valid – used in same scope. + a.Equal(0, result) + r.NoError(err) + + cases := []struct{ Name string }{} + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + a.Equal(0, result) // want "wrong-t: assertion object a was created with outer scope's testing\\.T" + r.NoError(err) // want "wrong-t: assertion object r was created with outer scope's testing\\.T" + + // Valid – created inside this subtest. + subA := assert.New(t) + subA.Equal(0, result) + }) + } + + t.Run("single", func(t *testing.T) { + a.Equal(0, result) // want "wrong-t: assertion object a was created with outer scope's testing\\.T" + + // Nested subtest – a is still from outer scope. + t.Run("nested", func(t *testing.T) { + a.Equal(0, result) // want "wrong-t: assertion object a was created with outer scope's testing\\.T" + + // Valid – b is created inside this nested subtest. + b := assert.New(t) + b.Equal(0, result) + }) + }) + + // Rebound to outer t (not the callback's t): still an error because the + // rebinding does not use the subtest's own t. + outerT := t + t.Run("rebind-wrong-t", func(t *testing.T) { + a = assert.New(outerT) + a.Equal(0, result) // want "wrong-t: assertion object a was created with outer scope's testing\\.T" + }) +} + +// Valid uses: object-style calls (no t argument needed), or t from same scope. +func TestWrongTChecker_Valid(t *testing.T) { + var err error + var result int + + a := assert.New(t) + r := require.New(t) + + a.Equal(0, result) + r.NoError(err) + + t.Run("sub", func(t *testing.T) { + assert.Equal(t, 0, result) + require.NoError(t, err) + + inner := assert.New(t) + inner.Equal(0, result) + }) + + // Valid – outer-scope assertion object rebound to subtest's t before use. + t.Run("rebound", func(t *testing.T) { + a = assert.New(t) + r = require.New(t) + a.Equal(0, result) + r.NoError(err) + }) +} diff --git a/internal/checkers/checkers_registry.go b/internal/checkers/checkers_registry.go index 264e18b6..8e332f31 100644 --- a/internal/checkers/checkers_registry.go +++ b/internal/checkers/checkers_registry.go @@ -33,6 +33,7 @@ var registry = checkersRegistry{ {factory: asCheckerFactory(NewSuiteMethodSignature), enabledByDefault: true}, {factory: asCheckerFactory(NewSuiteSubtestRun), enabledByDefault: true}, {factory: asCheckerFactory(NewSuiteTHelper), enabledByDefault: false}, + {factory: asCheckerFactory(NewWrongT), enabledByDefault: true}, } type checkersRegistry []checkerMeta diff --git a/internal/checkers/checkers_registry_test.go b/internal/checkers/checkers_registry_test.go index dc515a26..d4035eb4 100644 --- a/internal/checkers/checkers_registry_test.go +++ b/internal/checkers/checkers_registry_test.go @@ -60,6 +60,7 @@ func TestAll(t *testing.T) { "suite-method-signature", "suite-subtest-run", "suite-thelper", + "wrong-t", } if !slices.Equal(expected, checkerList) { t.Fatalf("unexpected list: %#v", checkerList) @@ -98,6 +99,7 @@ func TestEnabledByDefault(t *testing.T) { "suite-broken-parallel", "suite-method-signature", "suite-subtest-run", + "wrong-t", } if !slices.Equal(expected, checkerList) { t.Fatalf("unexpected list: %#v", checkerList) diff --git a/internal/checkers/wrong_t.go b/internal/checkers/wrong_t.go new file mode 100644 index 00000000..f5367378 --- /dev/null +++ b/internal/checkers/wrong_t.go @@ -0,0 +1,410 @@ +package checkers + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" + + "github.com/Antonboom/testifylint/internal/analysisutil" + "github.com/Antonboom/testifylint/internal/testify" +) + +const ( + wrongTNilReport = "do not pass nil as testing.T" + wrongTFreshReport = "do not pass freshly created testing.T" + wrongTScopeReport = "assertion object %s was created with outer scope's testing.T" +) + +// WrongT detects situations where testify assertions are called with a wrong testing.T. +// +// Case 1 (nil or freshly-created testing.T passed to package-level assertions): +// +// func TestFoo(t *testing.T) { +// assert.Equal(nil, expected, actual) ❌ +// +// u := &testing.T{} +// assert.Equal(u, expected, actual) ❌ +// } +// +// Case 2 (Assertions object created with outer t used inside t.Run subtest): +// +// func TestFoo(t *testing.T) { +// a := assert.New(t) +// t.Run("sub", func(t *testing.T) { +// a.Equal(expected, actual) ❌ a was created with outer t +// }) +// } +type WrongT struct{} + +// NewWrongT constructs WrongT checker. +func NewWrongT() *WrongT { return new(WrongT) } +func (WrongT) Name() string { return "wrong-t" } + +func (checker WrongT) Check(pass *analysis.Pass, insp *inspector.Inspector) (diagnostics []analysis.Diagnostic) { + // Phase 1: collect variables assigned from freshly-created *testing.T (for Case 1 indirect). + freshTVars := collectFreshTestingTVars(pass, insp) + + // Phase 2: collect variables assigned from assert.New / require.New (for Case 2). + assertVars := collectAssertionVars(pass, insp) + + // Phase 3: check package-level testify calls for nil / fresh testing.T (Case 1). + insp.Preorder([]ast.Node{(*ast.CallExpr)(nil)}, func(node ast.Node) { + ce := node.(*ast.CallExpr) + call := NewCallMeta(pass, ce) + if call == nil || !call.IsPkg { + return + } + + sig := call.Fn.Signature + if !sigExpectsTestingTFirstParam(sig) { + return + } + if len(call.ArgsRaw) == 0 { + return + } + tArg := call.ArgsRaw[0] + + if isNil(tArg) { + d := newDiagnostic(checker.Name(), call, wrongTNilReport) + diagnostics = append(diagnostics, *d) + return + } + + if isFreshTestingTExpr(pass, tArg) { + d := newDiagnostic(checker.Name(), call, wrongTFreshReport) + diagnostics = append(diagnostics, *d) + return + } + + if tArgIdent, ok := tArg.(*ast.Ident); ok { + obj := pass.TypesInfo.ObjectOf(tArgIdent) + if obj != nil && freshTVars[obj] { + d := newDiagnostic(checker.Name(), call, wrongTFreshReport) + diagnostics = append(diagnostics, *d) + } + } + }) + + // Phase 4: check t.Run callbacks for assertion object used from outer scope (Case 2). + insp.Preorder([]ast.Node{(*ast.CallExpr)(nil)}, func(node ast.Node) { + ce := node.(*ast.CallExpr) + if !isSubTestRun(pass, ce) { + return + } + + // Find function literal callback with a testing.T parameter. + var callback *ast.FuncLit + for _, arg := range ce.Args { + if fl, ok := arg.(*ast.FuncLit); ok && hasTestingTParam(pass, fl.Type) { + callback = fl + break + } + } + if callback == nil { + return + } + + callbackBodyStart := callback.Body.Lbrace + + // Obtain the types.Object for the callback's own *testing.T parameter so that + // we can verify rebindings actually use the subtest's t, not some other TestingT. + callbackTParam := testingTParamObj(pass, callback) + + // Collect the earliest position where each outer-scope assertion object is + // rebound inside the callback via a plain `=` assignment to assert.New / require.New, + // where the New call's first argument is the callback's own *testing.T parameter. + // E.g.: a = assert.New(t) — t is the callback's t, not an outer one. + // Calls on `a` that occur AFTER such a rebinding are suppressed to avoid false positives. + innerRebindings := collectInnerRebindings(pass, callback, callbackTParam, callbackBodyStart, assertVars) + + // Walk callback body looking for calls on assertion objects from outer scope. + // Don't recurse into nested function literals to avoid double-reporting – + // the inner t.Run callbacks are processed separately by this same loop. + ast.Inspect(callback.Body, func(n ast.Node) bool { + // Stop recursion at nested function literals. + if _, isFuncLit := n.(*ast.FuncLit); isFuncLit { + return false + } + callCE, ok := n.(*ast.CallExpr) + if !ok { + return true + } + se, ok := callCE.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + id, ok := se.X.(*ast.Ident) + if !ok { + return true + } + obj := pass.TypesInfo.ObjectOf(id) + if obj == nil { + return true + } + if assertVars[obj] && obj.Pos() < callbackBodyStart { + // Suppress if the object was rebound before this call. + if rebindPos, rebound := innerRebindings[obj]; rebound && rebindPos < callCE.Pos() { + return true + } + d := newDiagnostic(checker.Name(), callCE, + fmt.Sprintf(wrongTScopeReport, id.Name)) + diagnostics = append(diagnostics, *d) + } + return true + }) + }) + + return diagnostics +} + +// collectInnerRebindings collects the earliest position where each outer-scope assertion object +// is rebound inside the callback via a plain `=` assignment to assert.New / require.New, +// where the New call's first argument is the callback's own *testing.T parameter. +func collectInnerRebindings( + pass *analysis.Pass, + callback *ast.FuncLit, + callbackTParam types.Object, + callbackBodyStart token.Pos, + assertVars map[types.Object]bool, +) map[types.Object]token.Pos { + result := make(map[types.Object]token.Pos) + if callbackTParam == nil { + return result + } + ast.Inspect(callback.Body, func(n ast.Node) bool { + if _, isFuncLit := n.(*ast.FuncLit); isFuncLit { + return false + } + as, ok := n.(*ast.AssignStmt) + if !ok || as.Tok != token.ASSIGN { + return true + } + for i, rhs := range as.Rhs { + callExpr, ok := rhs.(*ast.CallExpr) + if !ok { + continue + } + cm := NewCallMeta(pass, callExpr) + if cm == nil || !cm.IsPkg || cm.Fn.Name != "New" { + continue + } + // Only suppress when assert.New is called with the callback's own t. + if len(cm.ArgsRaw) == 0 { + continue + } + argIdent, ok := cm.ArgsRaw[0].(*ast.Ident) + if !ok { + continue + } + if pass.TypesInfo.ObjectOf(argIdent) != callbackTParam { + continue + } + if i >= len(as.Lhs) { + continue + } + lhsID, ok := as.Lhs[i].(*ast.Ident) + if !ok { + continue + } + obj := pass.TypesInfo.ObjectOf(lhsID) + if obj == nil || !assertVars[obj] || obj.Pos() >= callbackBodyStart { + continue + } + // Record the earliest rebinding position for this object. + if existing, seen := result[obj]; !seen || as.Pos() < existing { + result[obj] = as.Pos() + } + } + return true + }) + return result +} + +// collectFreshTestingTVars collects all variables that are assigned from freshly created *testing.T. +// E.g.: u := &testing.T{} or u := new(testing.T). +// Variables that are subsequently reassigned (via =) to a non-fresh expression are excluded, +// to avoid false positives for patterns like: u := &testing.T{}; u = t; assert.Equal(u, ...). +func collectFreshTestingTVars(pass *analysis.Pass, insp *inspector.Inspector) map[types.Object]bool { + result := make(map[types.Object]bool) + + insp.Preorder([]ast.Node{(*ast.AssignStmt)(nil)}, func(node ast.Node) { + as := node.(*ast.AssignStmt) + switch as.Tok { + case token.DEFINE: + for i, rhs := range as.Rhs { + if !isFreshTestingTExpr(pass, rhs) { + continue + } + if i >= len(as.Lhs) { + continue + } + id, ok := as.Lhs[i].(*ast.Ident) + if !ok { + continue + } + obj := pass.TypesInfo.ObjectOf(id) + if obj != nil { + result[obj] = true + } + } + case token.ASSIGN: + // If a variable from result is reassigned to a non-fresh expression, remove it + // to avoid false positives when the variable is later used with a valid TestingT. + // For multi-return assignments (a, b = f()), where LHS and RHS counts differ, + // we cannot determine per-element freshness, so conservatively remove all + // LHS variables that are in the fresh set. + if len(as.Lhs) != len(as.Rhs) { + for _, lhs := range as.Lhs { + id, ok := lhs.(*ast.Ident) + if !ok { + continue + } + obj := pass.TypesInfo.ObjectOf(id) + if obj != nil { + delete(result, obj) + } + } + return + } + for i, lhs := range as.Lhs { + id, ok := lhs.(*ast.Ident) + if !ok { + continue + } + obj := pass.TypesInfo.ObjectOf(id) + if obj == nil || !result[obj] { + continue + } + if !isFreshTestingTExpr(pass, as.Rhs[i]) { + delete(result, obj) + } + } + default: + } + }) + + return result +} + +// collectAssertionVars collects all variables assigned from assert.New(t) or require.New(t). +func collectAssertionVars(pass *analysis.Pass, insp *inspector.Inspector) map[types.Object]bool { + result := make(map[types.Object]bool) + + insp.Preorder([]ast.Node{(*ast.AssignStmt)(nil)}, func(node ast.Node) { + as := node.(*ast.AssignStmt) + if as.Tok != token.DEFINE { + return + } + for i, rhs := range as.Rhs { + ce, ok := rhs.(*ast.CallExpr) + if !ok { + continue + } + cm := NewCallMeta(pass, ce) + if cm == nil || !cm.IsPkg || cm.Fn.Name != "New" { + continue + } + if i >= len(as.Lhs) { + continue + } + id, ok := as.Lhs[i].(*ast.Ident) + if !ok { + continue + } + obj := pass.TypesInfo.ObjectOf(id) + if obj != nil { + result[obj] = true + } + } + }) + + return result +} + +// sigExpectsTestingTFirstParam returns true if the function signature's first parameter is a testify TestingT. +func sigExpectsTestingTFirstParam(sig *types.Signature) bool { + if sig.Params().Len() == 0 { + return false + } + return isTestifyTestingTType(sig.Params().At(0).Type()) +} + +// isTestifyTestingTType returns true if t is the TestingT interface from testify/assert or testify/require. +func isTestifyTestingTType(t types.Type) bool { + named, ok := t.(*types.Named) + if !ok { + return false + } + obj := named.Obj() + if obj.Name() != "TestingT" { + return false + } + pkg := obj.Pkg() + if pkg == nil { + return false + } + return analysisutil.IsPkg(pkg, testify.AssertPkgName, testify.AssertPkgPath) || + analysisutil.IsPkg(pkg, testify.RequirePkgName, testify.RequirePkgPath) +} + +// isFreshTestingTExpr returns true if expr is a freshly created *testing.T: +// - &testing.T{} +// - new(testing.T) +func isFreshTestingTExpr(pass *analysis.Pass, expr ast.Expr) bool { + // Check for &testing.T{}. + if unary, ok := expr.(*ast.UnaryExpr); ok && unary.Op == token.AND { + if cl, ok := unary.X.(*ast.CompositeLit); ok { + return isTestingTTypeExpr(pass, cl.Type) + } + } + // Check for new(testing.T). + if ce, ok := expr.(*ast.CallExpr); ok { + if isIdentWithName("new", ce.Fun) && len(ce.Args) == 1 { + return isTestingTTypeExpr(pass, ce.Args[0]) + } + } + return false +} + +// isTestingTTypeExpr returns true if expr is the type expression testing.T. +func isTestingTTypeExpr(pass *analysis.Pass, expr ast.Expr) bool { + if expr == nil { + return false + } + se, ok := expr.(*ast.SelectorExpr) + if !ok { + return false + } + if se.Sel == nil || se.Sel.Name != "T" { + return false + } + obj := pass.TypesInfo.ObjectOf(se.Sel) + if obj == nil { + return false + } + pkg := obj.Pkg() + return pkg != nil && pkg.Path() == "testing" +} + +// testingTParamObj returns the types.Object for the first *testing.T parameter of a function literal, +// or nil if no such parameter exists. +func testingTParamObj(pass *analysis.Pass, fl *ast.FuncLit) types.Object { + if fl.Type == nil || fl.Type.Params == nil { + return nil + } + for _, field := range fl.Type.Params.List { + if !implementsTestingT(pass, field.Type) { + continue + } + for _, name := range field.Names { + if obj := pass.TypesInfo.ObjectOf(name); obj != nil { + return obj + } + } + } + return nil +} diff --git a/internal/testgen/gen_wrong_t.go b/internal/testgen/gen_wrong_t.go new file mode 100644 index 00000000..c576c5d6 --- /dev/null +++ b/internal/testgen/gen_wrong_t.go @@ -0,0 +1,177 @@ +package main + +import ( + "text/template" + + "github.com/Antonboom/testifylint/internal/checkers" +) + +type WrongTTestsGenerator struct{} + +func (WrongTTestsGenerator) Checker() checkers.Checker { + return checkers.NewWrongT() +} + +func (g WrongTTestsGenerator) TemplateData() any { + var ( + checker = g.Checker().Name() + nilReport = QuoteReport(checker + ": do not pass nil as testing.T") + freshReport = QuoteReport(checker + ": do not pass freshly created testing.T") + ) + + return struct { + CheckerName CheckerName + NilReport string + FreshReport string + Checker string + }{ + CheckerName: CheckerName(checker), + NilReport: nilReport, + FreshReport: freshReport, + Checker: checker, + } +} + +func (WrongTTestsGenerator) ErroredTemplate() Executor { + return template.Must(template.New("WrongTTestsGenerator.ErroredTemplate"). + Funcs(fm). + Parse(wrongTTestTmpl)) +} + +func (WrongTTestsGenerator) GoldenTemplate() Executor { + // No autofix available for these issues. + return nil +} + +const wrongTTestTmpl = header + ` + +package {{ .CheckerName.AsPkgName }} + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Case 1: nil testing.T passed to package-level assertions. +func {{ .CheckerName.AsTestName }}_NilT(t *testing.T) { + var err error + var result int + + assert.Equal(nil, 0, result) // want {{ $.NilReport }} + assert.NoError(nil, err) // want {{ $.NilReport }} + require.Equal(nil, 0, result) // want {{ $.NilReport }} + require.NoError(nil, err) // want {{ $.NilReport }} + + // Valid – using the test's t. + assert.Equal(t, 0, result) + assert.NoError(t, err) + require.Equal(t, 0, result) + require.NoError(t, err) + + t.Run("sub", func(t *testing.T) { + assert.Equal(t, 0, result) + require.NoError(t, err) + }) +} + +// Case 2: freshly created testing.T passed to package-level assertions. +func {{ .CheckerName.AsTestName }}_FreshT(t *testing.T) { + var err error + var result int + + assert.Equal(&testing.T{}, 0, result) // want {{ $.FreshReport }} + assert.NoError(new(testing.T), err) // want {{ $.FreshReport }} + require.Equal(&testing.T{}, 0, result) // want {{ $.FreshReport }} + require.NoError(new(testing.T), err) // want {{ $.FreshReport }} + + u := &testing.T{} + assert.Equal(u, 0, result) // want {{ $.FreshReport }} + require.Equal(u, 0, result) // want {{ $.FreshReport }} + + v := new(testing.T) + assert.NoError(v, err) // want {{ $.FreshReport }} + require.NoError(v, err) // want {{ $.FreshReport }} + + // Valid – fresh variable reassigned to a real t before use. + w := &testing.T{} + w = t + assert.Equal(w, 0, result) + require.NoError(w, err) +} + +// Case 3: assertion object created with outer t used inside t.Run subtest. +func {{ .CheckerName.AsTestName }}_Scope(t *testing.T) { + a := assert.New(t) + r := require.New(t) + + var err error + var result int + + // Valid – used in same scope. + a.Equal(0, result) + r.NoError(err) + + cases := []struct{ Name string }{} + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + a.Equal(0, result) // want {{ QuoteReport (printf "%s: assertion object a was created with outer scope's testing.T" $.Checker) }} + r.NoError(err) // want {{ QuoteReport (printf "%s: assertion object r was created with outer scope's testing.T" $.Checker) }} + + // Valid – created inside this subtest. + subA := assert.New(t) + subA.Equal(0, result) + }) + } + + t.Run("single", func(t *testing.T) { + a.Equal(0, result) // want {{ QuoteReport (printf "%s: assertion object a was created with outer scope's testing.T" $.Checker) }} + + // Nested subtest – a is still from outer scope. + t.Run("nested", func(t *testing.T) { + a.Equal(0, result) // want {{ QuoteReport (printf "%s: assertion object a was created with outer scope's testing.T" $.Checker) }} + + // Valid – b is created inside this nested subtest. + b := assert.New(t) + b.Equal(0, result) + }) + }) + + // Rebound to outer t (not the callback's t): still an error because the + // rebinding does not use the subtest's own t. + outerT := t + t.Run("rebind-wrong-t", func(t *testing.T) { + a = assert.New(outerT) + a.Equal(0, result) // want {{ QuoteReport (printf "%s: assertion object a was created with outer scope's testing.T" $.Checker) }} + }) +} + +// Valid uses: object-style calls (no t argument needed), or t from same scope. +func {{ .CheckerName.AsTestName }}_Valid(t *testing.T) { + var err error + var result int + + a := assert.New(t) + r := require.New(t) + + a.Equal(0, result) + r.NoError(err) + + t.Run("sub", func(t *testing.T) { + assert.Equal(t, 0, result) + require.NoError(t, err) + + inner := assert.New(t) + inner.Equal(0, result) + }) + + // Valid – outer-scope assertion object rebound to subtest's t before use. + t.Run("rebound", func(t *testing.T) { + a = assert.New(t) + r = require.New(t) + a.Equal(0, result) + r.NoError(err) + }) +} +` diff --git a/internal/testgen/main.go b/internal/testgen/main.go index 027f4a52..4403801d 100644 --- a/internal/testgen/main.go +++ b/internal/testgen/main.go @@ -49,6 +49,7 @@ var checkerTestsGenerators = []CheckerTestsGenerator{ SuiteSubtestRunTestsGenerator{}, SuiteTHelperTestsGenerator{}, UselessAssertTestsGenerator{}, + WrongTTestsGenerator{}, } func init() {