diff --git a/.golangci.next.reference.yml b/.golangci.next.reference.yml index 632605787381..19e12f425736 100644 --- a/.golangci.next.reference.yml +++ b/.golangci.next.reference.yml @@ -44,6 +44,7 @@ linters: - errorlint - exhaustive - exhaustruct + - expecterlint - exptostd - fatcontext - forbidigo @@ -155,6 +156,7 @@ linters: - errorlint - exhaustive - exhaustruct + - expecterlint - exptostd - fatcontext - forbidigo diff --git a/go.mod b/go.mod index 0ea80a835489..f254794f2bb9 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/ckaznocha/intrange v0.3.1 github.com/curioswitch/go-reassign v0.3.0 + github.com/d0ubletr0uble/expecterlint v1.1.0 github.com/daixiang0/gci v0.13.6 github.com/denis-tingaikin/go-header v0.5.0 github.com/fatih/color v1.18.0 diff --git a/go.sum b/go.sum index dab2774e9ca2..52f4aefc1773 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= +github.com/d0ubletr0uble/expecterlint v1.1.0 h1:HnVQnMj6SnT3fVkUvtAK+NuZzX3WAitOHOtqAD+1xi8= +github.com/d0ubletr0uble/expecterlint v1.1.0/go.mod h1:D34qv3UHQWSirrLVBLWXveP/gaik3vISiHKlrZTOXa8= github.com/daixiang0/gci v0.13.6 h1:RKuEOSkGpSadkGbvZ6hJ4ddItT3cVZ9Vn9Rybk6xjl8= github.com/daixiang0/gci v0.13.6/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= diff --git a/jsonschema/golangci.next.jsonschema.json b/jsonschema/golangci.next.jsonschema.json index 3e51264fec47..73d938f443ce 100644 --- a/jsonschema/golangci.next.jsonschema.json +++ b/jsonschema/golangci.next.jsonschema.json @@ -771,6 +771,7 @@ "errorlint", "exhaustive", "exhaustruct", + "expecterlint", "exptostd", "fatcontext", "forbidigo", diff --git a/pkg/golinters/expecterlint/expecterlint.go b/pkg/golinters/expecterlint/expecterlint.go new file mode 100644 index 000000000000..2c0e6299b0fe --- /dev/null +++ b/pkg/golinters/expecterlint/expecterlint.go @@ -0,0 +1,13 @@ +package expecterlint + +import ( + "github.com/d0ubletr0uble/expecterlint" + + "github.com/golangci/golangci-lint/v2/pkg/goanalysis" +) + +func New() *goanalysis.Linter { + return goanalysis. + NewLinterFromAnalyzer(expecterlint.Analyzer). + WithLoadMode(goanalysis.LoadModeTypesInfo) +} diff --git a/pkg/golinters/expecterlint/expecterlint_integration_test.go b/pkg/golinters/expecterlint/expecterlint_integration_test.go new file mode 100644 index 000000000000..3ae795730e74 --- /dev/null +++ b/pkg/golinters/expecterlint/expecterlint_integration_test.go @@ -0,0 +1,15 @@ +package expecterlint + +import ( + "testing" + + "github.com/golangci/golangci-lint/v2/test/testshared/integration" +) + +func TestFromTestdata(t *testing.T) { + integration.RunTestdata(t) +} + +func TestFix(t *testing.T) { + integration.RunFix(t) +} diff --git a/pkg/golinters/expecterlint/testdata/expecterlint_test.go b/pkg/golinters/expecterlint/testdata/expecterlint_test.go new file mode 100644 index 000000000000..ecf15bb11b38 --- /dev/null +++ b/pkg/golinters/expecterlint/testdata/expecterlint_test.go @@ -0,0 +1,268 @@ +//golangcitest:args -Eexpecterlint +package testdata + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" +) + +type MockUserIFace struct { + mock.Mock +} + +type MockUserIFace_Expecter struct { + mock *mock.Mock +} + +func (_m *MockUserIFace) EXPECT() *MockUserIFace_Expecter { + return &MockUserIFace_Expecter{mock: &_m.Mock} +} + +// CountUsers provides a mock function with no fields +func (_m *MockUserIFace) CountUsers() int { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for CountUsers") + } + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// MockUserIFace_CountUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CountUsers' +type MockUserIFace_CountUsers_Call struct { + *mock.Call +} + +// CountUsers is a helper method to define mock.On call +func (_e *MockUserIFace_Expecter) CountUsers() *MockUserIFace_CountUsers_Call { + return &MockUserIFace_CountUsers_Call{Call: _e.mock.On("CountUsers")} +} + +func (_c *MockUserIFace_CountUsers_Call) Run(run func()) *MockUserIFace_CountUsers_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockUserIFace_CountUsers_Call) Return(_a0 int) *MockUserIFace_CountUsers_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockUserIFace_CountUsers_Call) RunAndReturn(run func() int) *MockUserIFace_CountUsers_Call { + _c.Call.Return(run) + return _c +} + +// CreateUser provides a mock function with given fields: _a0, _a1 +func (_m *MockUserIFace) CreateUser(_a0 context.Context, _a1 User) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for CreateUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, User) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockUserIFace_CreateUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateUser' +type MockUserIFace_CreateUser_Call struct { + *mock.Call +} + +// CreateUser is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 User +func (_e *MockUserIFace_Expecter) CreateUser(_a0 interface{}, _a1 interface{}) *MockUserIFace_CreateUser_Call { + return &MockUserIFace_CreateUser_Call{Call: _e.mock.On("CreateUser", _a0, _a1)} +} + +func (_c *MockUserIFace_CreateUser_Call) Run(run func(_a0 context.Context, _a1 User)) *MockUserIFace_CreateUser_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(User)) + }) + return _c +} + +func (_c *MockUserIFace_CreateUser_Call) Return(_a0 error) *MockUserIFace_CreateUser_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockUserIFace_CreateUser_Call) RunAndReturn(run func(context.Context, User) error) *MockUserIFace_CreateUser_Call { + _c.Call.Return(run) + return _c +} + +// GetUser provides a mock function with given fields: ctx, name +func (_m *MockUserIFace) GetUser(ctx context.Context, name string) (User, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetUser") + } + + var r0 User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (User, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) User); ok { + r0 = rf(ctx, name) + } else { + r0 = ret.Get(0).(User) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUserIFace_GetUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUser' +type MockUserIFace_GetUser_Call struct { + *mock.Call +} + +// GetUser is a helper method to define mock.On call +// - ctx context.Context +// - name string +func (_e *MockUserIFace_Expecter) GetUser(ctx interface{}, name interface{}) *MockUserIFace_GetUser_Call { + return &MockUserIFace_GetUser_Call{Call: _e.mock.On("GetUser", ctx, name)} +} + +func (_c *MockUserIFace_GetUser_Call) Run(run func(ctx context.Context, name string)) *MockUserIFace_GetUser_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockUserIFace_GetUser_Call) Return(_a0 User, _a1 error) *MockUserIFace_GetUser_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUserIFace_GetUser_Call) RunAndReturn(run func(context.Context, string) (User, error)) *MockUserIFace_GetUser_Call { + _c.Call.Return(run) + return _c +} + +// Void provides a mock function with no fields +func (_m *MockUserIFace) Void() { + _m.Called() +} + +// MockUserIFace_Void_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Void' +type MockUserIFace_Void_Call struct { + *mock.Call +} + +// Void is a helper method to define mock.On call +func (_e *MockUserIFace_Expecter) Void() *MockUserIFace_Void_Call { + return &MockUserIFace_Void_Call{Call: _e.mock.On("Void")} +} + +func (_c *MockUserIFace_Void_Call) Run(run func()) *MockUserIFace_Void_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockUserIFace_Void_Call) Return() *MockUserIFace_Void_Call { + _c.Call.Return() + return _c +} + +func (_c *MockUserIFace_Void_Call) RunAndReturn(run func()) *MockUserIFace_Void_Call { + _c.Run(run) + return _c +} + +// NewMockUserIFace creates a new instance of MockUserIFace. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockUserIFace(t interface { + mock.TestingT + Cleanup(func()) +}) *MockUserIFace { + mock := &MockUserIFace{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +type User struct { + Name string + Age int +} + +func Test_CreateUser(t *testing.T) { + u := NewMockUserIFace(t) + u.On("CreateUser", mock.Anything, User{}).Return(nil) // want `mock\.On\(\"CreateUser\", \.\.\.\) could be replaced with mock\.EXPECT\(\)\.CreateUser\(\.\.\.\)` + + err := u.CreateUser(context.Background(), User{}) + if err != nil { + t.Error(err) + } +} + +func Test_GetUser(t *testing.T) { + userMock := &MockUserIFace{} + userMock. // want `mock\.On\(\"GetUser\", \.\.\.\) could be replaced with mock\.EXPECT\(\)\.GetUser\(\.\.\.\)` + On( + "GetUser", + context.Background(), + "test", + ).Return(User{}, nil) +} + +func Test_Expecter(t *testing.T) { + u := NewMockUserIFace(t) + u.EXPECT().GetUser(context.Background(), "Bob").Return(User{}, nil) // OK +} + +func Test_EmptyMethod(t *testing.T) { + m := NewMockUserIFace(t) + m.On("", mock.Anything, User{}).Return(nil) // ignore empty method name +} + +func Test_InvalidMethod(t *testing.T) { + i := NewMockUserIFace(t) + // no function i.MOCK().DoesNotExist(...) + i.On("DoesNotExist", mock.Anything, User{}, 123).Return(nil) +} + +func Test_Void(t *testing.T) { + u := NewMockUserIFace(t) + u.On("Void") // want `mock\.On\(\"Void\", \.\.\.\) could be replaced with mock\.EXPECT\(\)\.Void\(\.\.\.\)` + u.On("Void").Once() // want `mock\.On\(\"Void\", \.\.\.\) could be replaced with mock\.EXPECT\(\)\.Void\(\.\.\.\)` +} + +func Test_Count(t *testing.T) { + u := NewMockUserIFace(t) + u.On("CountUsers").Return(123) // want `mock\.On\(\"CountUsers\", \.\.\.\) could be replaced with mock\.EXPECT\(\)\.CountUsers\(\.\.\.\)` +} diff --git a/pkg/golinters/expecterlint/testdata/fix/in/expecterlint_test.go b/pkg/golinters/expecterlint/testdata/fix/in/expecterlint_test.go new file mode 100644 index 000000000000..3a60047f9499 --- /dev/null +++ b/pkg/golinters/expecterlint/testdata/fix/in/expecterlint_test.go @@ -0,0 +1,30 @@ +//golangcitest:args -Eexpecterlint +//golangcitest:expected_exitcode 0 +package testdata + +import ( + "testing" +) + +type Mock struct{} + +func (m *Mock) On(string) *Mock { + return m +} + +func (m *Mock) EXPECT() *Mock { + return m +} + +func (m *Mock) Return(bool) *Mock { + return m +} + +func (m *Mock) IsActive() *Mock { + return m +} + +func Test_GetUser(t *testing.T) { + m := &Mock{} + m.On("IsActive").Return(true) +} diff --git a/pkg/golinters/expecterlint/testdata/fix/out/expecterlint_test.go b/pkg/golinters/expecterlint/testdata/fix/out/expecterlint_test.go new file mode 100644 index 000000000000..ef97864a54a2 --- /dev/null +++ b/pkg/golinters/expecterlint/testdata/fix/out/expecterlint_test.go @@ -0,0 +1,30 @@ +//golangcitest:args -Eexpecterlint +//golangcitest:expected_exitcode 0 +package testdata + +import ( + "testing" +) + +type Mock struct{} + +func (m *Mock) On(string) *Mock { + return m +} + +func (m *Mock) EXPECT() *Mock { + return m +} + +func (m *Mock) Return(bool) *Mock { + return m +} + +func (m *Mock) IsActive() *Mock { + return m +} + +func Test_GetUser(t *testing.T) { + m := &Mock{} + m.EXPECT().IsActive().Return(true) +} diff --git a/pkg/lint/lintersdb/builder_linter.go b/pkg/lint/lintersdb/builder_linter.go index df84ad737966..21c9849564e2 100644 --- a/pkg/lint/lintersdb/builder_linter.go +++ b/pkg/lint/lintersdb/builder_linter.go @@ -27,6 +27,7 @@ import ( "github.com/golangci/golangci-lint/v2/pkg/golinters/errorlint" "github.com/golangci/golangci-lint/v2/pkg/golinters/exhaustive" "github.com/golangci/golangci-lint/v2/pkg/golinters/exhaustruct" + "github.com/golangci/golangci-lint/v2/pkg/golinters/expecterlint" "github.com/golangci/golangci-lint/v2/pkg/golinters/exptostd" "github.com/golangci/golangci-lint/v2/pkg/golinters/fatcontext" "github.com/golangci/golangci-lint/v2/pkg/golinters/forbidigo" @@ -250,6 +251,12 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithLoadForGoAnalysis(). WithURL("https://github.com/GaijinEntertainment/go-exhaustruct"), + linter.NewConfig(expecterlint.New()). + WithSince("v2.3.0"). + WithLoadForGoAnalysis(). + WithAutoFix(). + WithURL("https://github.com/d0ubletr0uble/expecterlint"), + linter.NewConfig(exptostd.New()). WithSince("v1.63.0"). WithLoadForGoAnalysis().