diff --git a/.mockery.yaml b/.mockery.yaml index 2559bb86..ffeed52b 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -41,6 +41,17 @@ packages: config: with-expecter: True unroll-variadic: False + # Replace generic params with a new constraint and a new fixed value + ReplaceGeneric: + config: + replace-type: + - github.com/vektra/mockery/v2/pkg/fixtures.ReplaceGeneric[-TImport]=github.com/vektra/mockery/v2/pkg/fixtures/redefined_type_b.B + - github.com/vektra/mockery/v2/pkg/fixtures.ReplaceGeneric[TConstraint]=github.com/vektra/mockery/v2/pkg/fixtures/constraints.String + # Replace a generic param with the parent type + ReplaceGenericSelf: + config: + replace-type: + - github.com/vektra/mockery/v2/pkg/fixtures.ReplaceGenericSelf[-T]=github.com/vektra/mockery/v2/pkg/fixtures.*ReplaceGenericSelf github.com/vektra/mockery/v2/pkg/fixtures/recursive_generation: config: recursive: True diff --git a/Taskfile.yml b/Taskfile.yml index 5e11bb39..4823817c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -13,7 +13,7 @@ tasks: - "**/*.go" cmds: - go fmt ./... - + mocks: desc: generate new mocks from scratch deps: [mocks.remove, mocks.generate] diff --git a/docs/features.md b/docs/features.md index 9d320f98..bb6e9d94 100644 --- a/docs/features.md +++ b/docs/features.md @@ -90,6 +90,63 @@ func (_m *Handler) HandleMessage(m pubsub.Message) error { } ``` +Generic type constraints can also be replaced by targeting the changed parameter with the square bracket notation on the left hand side. + +```shell +mockery --replace-type github.com/vektra/mockery/v2/baz/internal/foo.InternalBaz[T]=github.com/vektra/mockery/v2/baz.Baz +``` + +For example: + +```go +type InternalBaz[T any] struct{} + +func (*InternalBaz[T]) Foo() T {} + +// Becomes +type InternalBaz[T baz.Baz] struct{} + +func (*InternalBaz[T]) Foo() T {} +``` + +If a type constraint needs to be removed and replaced with a type, target the constraint with square brackets and include a '-' in front to have it removed. + +```shell +mockery --replace-type github.com/vektra/mockery/v2/baz/internal/foo.InternalBaz[-T]=github.com/vektra/mockery/v2/baz.Baz +``` + +For example: + +```go +type InternalBaz[T any] struct{} + +func (*InternalBaz[T]) Foo() T {} + +// Becomes +type InternalBaz struct{} + +func (*InternalBaz) Foo() baz.Baz {} +``` + +When replacing a generic constraint, you can replace the type with a pointer by adding a '*' before the output type name. + +```shell +mockery --replace-type github.com/vektra/mockery/v2/baz/internal/foo.InternalBaz[-T]=github.com/vektra/mockery/v2/baz.*Baz +``` + +For example: + +```go +type InternalBaz[T any] struct{} + +func (*InternalBaz[T]) Foo() T {} + +// Becomes +type InternalBaz struct{} + +func (*InternalBaz) Foo() *baz.Baz {} +``` + `packages` configuration ------------------------ :octicons-tag-24: v2.21.0 diff --git a/mocks/github.com/vektra/mockery/v2/pkg/fixtures/ReplaceGeneric.go b/mocks/github.com/vektra/mockery/v2/pkg/fixtures/ReplaceGeneric.go new file mode 100644 index 00000000..0727b975 --- /dev/null +++ b/mocks/github.com/vektra/mockery/v2/pkg/fixtures/ReplaceGeneric.go @@ -0,0 +1,173 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + constraints "github.com/vektra/mockery/v2/pkg/fixtures/constraints" + + test "github.com/vektra/mockery/v2/pkg/fixtures/redefined_type_b" +) + +// ReplaceGeneric is an autogenerated mock type for the ReplaceGeneric type +type ReplaceGeneric[TConstraint constraints.String, TKeep interface{}] struct { + mock.Mock +} + +type ReplaceGeneric_Expecter[TConstraint constraints.String, TKeep interface{}] struct { + mock *mock.Mock +} + +func (_m *ReplaceGeneric[TConstraint, TKeep]) EXPECT() *ReplaceGeneric_Expecter[TConstraint, TKeep] { + return &ReplaceGeneric_Expecter[TConstraint, TKeep]{mock: &_m.Mock} +} + +// A provides a mock function with given fields: t1 +func (_m *ReplaceGeneric[TConstraint, TKeep]) A(t1 test.B) TKeep { + ret := _m.Called(t1) + + if len(ret) == 0 { + panic("no return value specified for A") + } + + var r0 TKeep + if rf, ok := ret.Get(0).(func(test.B) TKeep); ok { + r0 = rf(t1) + } else { + r0 = ret.Get(0).(TKeep) + } + + return r0 +} + +// ReplaceGeneric_A_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'A' +type ReplaceGeneric_A_Call[TConstraint constraints.String, TKeep interface{}] struct { + *mock.Call +} + +// A is a helper method to define mock.On call +// - t1 test.B +func (_e *ReplaceGeneric_Expecter[TConstraint, TKeep]) A(t1 interface{}) *ReplaceGeneric_A_Call[TConstraint, TKeep] { + return &ReplaceGeneric_A_Call[TConstraint, TKeep]{Call: _e.mock.On("A", t1)} +} + +func (_c *ReplaceGeneric_A_Call[TConstraint, TKeep]) Run(run func(t1 test.B)) *ReplaceGeneric_A_Call[TConstraint, TKeep] { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(test.B)) + }) + return _c +} + +func (_c *ReplaceGeneric_A_Call[TConstraint, TKeep]) Return(_a0 TKeep) *ReplaceGeneric_A_Call[TConstraint, TKeep] { + _c.Call.Return(_a0) + return _c +} + +func (_c *ReplaceGeneric_A_Call[TConstraint, TKeep]) RunAndReturn(run func(test.B) TKeep) *ReplaceGeneric_A_Call[TConstraint, TKeep] { + _c.Call.Return(run) + return _c +} + +// B provides a mock function with given fields: +func (_m *ReplaceGeneric[TConstraint, TKeep]) B() test.B { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for B") + } + + var r0 test.B + if rf, ok := ret.Get(0).(func() test.B); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(test.B) + } + + return r0 +} + +// ReplaceGeneric_B_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'B' +type ReplaceGeneric_B_Call[TConstraint constraints.String, TKeep interface{}] struct { + *mock.Call +} + +// B is a helper method to define mock.On call +func (_e *ReplaceGeneric_Expecter[TConstraint, TKeep]) B() *ReplaceGeneric_B_Call[TConstraint, TKeep] { + return &ReplaceGeneric_B_Call[TConstraint, TKeep]{Call: _e.mock.On("B")} +} + +func (_c *ReplaceGeneric_B_Call[TConstraint, TKeep]) Run(run func()) *ReplaceGeneric_B_Call[TConstraint, TKeep] { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ReplaceGeneric_B_Call[TConstraint, TKeep]) Return(_a0 test.B) *ReplaceGeneric_B_Call[TConstraint, TKeep] { + _c.Call.Return(_a0) + return _c +} + +func (_c *ReplaceGeneric_B_Call[TConstraint, TKeep]) RunAndReturn(run func() test.B) *ReplaceGeneric_B_Call[TConstraint, TKeep] { + _c.Call.Return(run) + return _c +} + +// C provides a mock function with given fields: +func (_m *ReplaceGeneric[TConstraint, TKeep]) C() TConstraint { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for C") + } + + var r0 TConstraint + if rf, ok := ret.Get(0).(func() TConstraint); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(TConstraint) + } + + return r0 +} + +// ReplaceGeneric_C_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'C' +type ReplaceGeneric_C_Call[TConstraint constraints.String, TKeep interface{}] struct { + *mock.Call +} + +// C is a helper method to define mock.On call +func (_e *ReplaceGeneric_Expecter[TConstraint, TKeep]) C() *ReplaceGeneric_C_Call[TConstraint, TKeep] { + return &ReplaceGeneric_C_Call[TConstraint, TKeep]{Call: _e.mock.On("C")} +} + +func (_c *ReplaceGeneric_C_Call[TConstraint, TKeep]) Run(run func()) *ReplaceGeneric_C_Call[TConstraint, TKeep] { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ReplaceGeneric_C_Call[TConstraint, TKeep]) Return(_a0 TConstraint) *ReplaceGeneric_C_Call[TConstraint, TKeep] { + _c.Call.Return(_a0) + return _c +} + +func (_c *ReplaceGeneric_C_Call[TConstraint, TKeep]) RunAndReturn(run func() TConstraint) *ReplaceGeneric_C_Call[TConstraint, TKeep] { + _c.Call.Return(run) + return _c +} + +// NewReplaceGeneric creates a new instance of ReplaceGeneric. 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 NewReplaceGeneric[TConstraint constraints.String, TKeep interface{}](t interface { + mock.TestingT + Cleanup(func()) +}) *ReplaceGeneric[TConstraint, TKeep] { + mock := &ReplaceGeneric[TConstraint, TKeep]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/github.com/vektra/mockery/v2/pkg/fixtures/ReplaceGenericSelf.go b/mocks/github.com/vektra/mockery/v2/pkg/fixtures/ReplaceGenericSelf.go new file mode 100644 index 00000000..8d2ef76e --- /dev/null +++ b/mocks/github.com/vektra/mockery/v2/pkg/fixtures/ReplaceGenericSelf.go @@ -0,0 +1,77 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// ReplaceGenericSelf is an autogenerated mock type for the ReplaceGenericSelf type +type ReplaceGenericSelf struct { + mock.Mock +} + +type ReplaceGenericSelf_Expecter struct { + mock *mock.Mock +} + +func (_m *ReplaceGenericSelf) EXPECT() *ReplaceGenericSelf_Expecter { + return &ReplaceGenericSelf_Expecter{mock: &_m.Mock} +} + +// A provides a mock function with given fields: +func (_m *ReplaceGenericSelf) A() *ReplaceGenericSelf { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for A") + } + + var r0 *ReplaceGenericSelf + if rf, ok := ret.Get(0).(func() *ReplaceGenericSelf); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(*ReplaceGenericSelf) + } + + return r0 +} + +// ReplaceGenericSelf_A_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'A' +type ReplaceGenericSelf_A_Call struct { + *mock.Call +} + +// A is a helper method to define mock.On call +func (_e *ReplaceGenericSelf_Expecter) A() *ReplaceGenericSelf_A_Call { + return &ReplaceGenericSelf_A_Call{Call: _e.mock.On("A")} +} + +func (_c *ReplaceGenericSelf_A_Call) Run(run func()) *ReplaceGenericSelf_A_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ReplaceGenericSelf_A_Call) Return(_a0 *ReplaceGenericSelf) *ReplaceGenericSelf_A_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ReplaceGenericSelf_A_Call) RunAndReturn(run func() *ReplaceGenericSelf) *ReplaceGenericSelf_A_Call { + _c.Call.Return(run) + return _c +} + +// NewReplaceGenericSelf creates a new instance of ReplaceGenericSelf. 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 NewReplaceGenericSelf(t interface { + mock.TestingT + Cleanup(func()) +}) *ReplaceGenericSelf { + mock := &ReplaceGenericSelf{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/github.com/vektra/mockery/v2/pkg/fixtures/Variadic.go b/mocks/github.com/vektra/mockery/v2/pkg/fixtures/Variadic.go index 4ee76f41..6b045d43 100644 --- a/mocks/github.com/vektra/mockery/v2/pkg/fixtures/Variadic.go +++ b/mocks/github.com/vektra/mockery/v2/pkg/fixtures/Variadic.go @@ -21,6 +21,10 @@ func (_m *Variadic) EXPECT() *Variadic_Expecter { func (_m *Variadic) VariadicFunction(str string, vFunc func(string, ...interface{}) interface{}) error { ret := _m.Called(str, vFunc) + if len(ret) == 0 { + panic("no return value specified for VariadicFunction") + } + var r0 error if rf, ok := ret.Get(0).(func(string, func(string, ...interface{}) interface{}) error); ok { r0 = rf(str, vFunc) diff --git a/mocks/github.com/vektra/mockery/v2/pkg/fixtures/VariadicReturnFunc.go b/mocks/github.com/vektra/mockery/v2/pkg/fixtures/VariadicReturnFunc.go index 181b07d9..b2c4918e 100644 --- a/mocks/github.com/vektra/mockery/v2/pkg/fixtures/VariadicReturnFunc.go +++ b/mocks/github.com/vektra/mockery/v2/pkg/fixtures/VariadicReturnFunc.go @@ -21,6 +21,10 @@ func (_m *VariadicReturnFunc) EXPECT() *VariadicReturnFunc_Expecter { func (_m *VariadicReturnFunc) SampleMethod(str string) func(string, []int, ...interface{}) { ret := _m.Called(str) + if len(ret) == 0 { + panic("no return value specified for SampleMethod") + } + var r0 func(string, []int, ...interface{}) if rf, ok := ret.Get(0).(func(string) func(string, []int, ...interface{})); ok { r0 = rf(str) diff --git a/pkg/fixtures/constraints/constraints.go b/pkg/fixtures/constraints/constraints.go index a17dc2e0..b73c7bb7 100644 --- a/pkg/fixtures/constraints/constraints.go +++ b/pkg/fixtures/constraints/constraints.go @@ -7,3 +7,7 @@ type Signed interface { type Integer interface { ~int } + +type String interface { + ~string +} diff --git a/pkg/fixtures/generic.go b/pkg/fixtures/generic.go index 802becb5..7589e520 100644 --- a/pkg/fixtures/generic.go +++ b/pkg/fixtures/generic.go @@ -36,3 +36,17 @@ type GetInt interface{ Get() int } type GetGeneric[T constraints.Integer] interface{ Get() T } type EmbeddedGet[T constraints.Signed] interface{ GetGeneric[T] } + +type ReplaceGeneric[ + TImport any, + TConstraint constraints.Signed, + TKeep any, +] interface { + A(t1 TImport) TKeep + B() TImport + C() TConstraint +} + +type ReplaceGenericSelf[T any] interface { + A() T +} diff --git a/pkg/fixtures/test/generic_test.go b/pkg/fixtures/test/generic_test.go new file mode 100644 index 00000000..66365a97 --- /dev/null +++ b/pkg/fixtures/test/generic_test.go @@ -0,0 +1,30 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + mocks "github.com/vektra/mockery/v2/mocks/github.com/vektra/mockery/v2/pkg/fixtures" + rtb "github.com/vektra/mockery/v2/pkg/fixtures/redefined_type_b" +) + +func TestReplaceGeneric(t *testing.T) { + type str string + + m := mocks.NewReplaceGeneric[str, str](t) + + m.EXPECT().A(rtb.B(1)).Return("") + assert.Equal(t, m.A(rtb.B(1)), str("")) + + m.EXPECT().B().Return(2) + assert.Equal(t, m.B(), rtb.B(2)) + + m.EXPECT().C().Return("") + assert.Equal(t, m.C(), str("")) +} + +func TestReplaceGenericSelf(t *testing.T) { + m := mocks.NewReplaceGenericSelf(t) + m.EXPECT().A().Return(m) + assert.Equal(t, m.A(), m) +} diff --git a/pkg/generator.go b/pkg/generator.go index 85ce89ee..26622271 100644 --- a/pkg/generator.go +++ b/pkg/generator.go @@ -196,7 +196,7 @@ func (g *Generator) addPackageImportWithName(ctx context.Context, path, name str log := zerolog.Ctx(ctx) replaced := false g.checkReplaceType(ctx, func(from *replaceType, to *replaceType) bool { - if o != nil && path == from.pkg && (from.typ == "" || o.Name() == from.typ) { + if o != nil && path == from.pkg && (from.typ == "" || o.Name() == from.typ || o.Name() == from.param) { log.Debug().Str("from", path).Str("to", to.pkg).Msg("changing package path") replaced = true path = to.pkg @@ -326,10 +326,32 @@ func (g *Generator) getTypeConstraintString(ctx context.Context) string { return "" } qualifiedParams := make([]string, 0, tp.Len()) +param: for i := 0; i < tp.Len(); i++ { param := tp.At(i) - qualifiedParams = append(qualifiedParams, fmt.Sprintf("%s %s", param.String(), g.renderType(ctx, param.Constraint()))) + str := param.String() + typ := g.renderType(ctx, param.Constraint()) + + for _, t := range g.replaceTypeCache { + if str == t.from.param { + // Skip removed generic constraints + if t.from.rmvParam { + continue param + } + + // Import replaced generic constraints + pkg := g.addPackageImportWithName(ctx, t.to.pkg, t.to.alias, param.Obj()) + typ = pkg + "." + t.to.typ + } + } + + qualifiedParams = append(qualifiedParams, fmt.Sprintf("%s %s", str, typ)) + } + + if len(qualifiedParams) == 0 { + return "" } + return fmt.Sprintf("[%s]", strings.Join(qualifiedParams, ", ")) } @@ -342,8 +364,21 @@ func (g *Generator) getInstantiatedTypeString() string { return "" } params := make([]string, 0, tp.Len()) +param: for i := 0; i < tp.Len(); i++ { - params = append(params, tp.At(i).String()) + str := tp.At(i).String() + + // Skip replaced generic types + for _, t := range g.replaceTypeCache { + if str == t.from.param && t.from.rmvParam { + continue param + } + } + + params = append(params, str) + } + if len(params) == 0 { + return "" } return fmt.Sprintf("[%s]", strings.Join(params, ", ")) } @@ -475,7 +510,25 @@ func (g *Generator) renderType(ctx context.Context, typ types.Type) string { return fmt.Sprintf("%s[%s]", name, strings.Join(args, ",")) case *types.TypeParam: if t.Constraint() != nil { - return t.Obj().Name() + name := t.Obj().Name() + pkg := "" + + g.checkReplaceType(ctx, func(from *replaceType, to *replaceType) bool { + // Replace with the new type if it is being removed as a constraint + if t.Obj().Pkg().Path() == from.pkg && name == from.param && from.rmvParam { + name = to.typ + if to.pkg != from.pkg { + pkg = g.addPackageImport(ctx, t.Obj().Pkg(), t.Obj()) + } + return false + } + return true + }) + + if pkg != "" { + return pkg + "." + name + } + return name } return g.getPackageScopedType(ctx, t.Obj()) case *types.Basic: @@ -1088,9 +1141,11 @@ func resolveCollision(names []string, variable string) string { } type replaceType struct { - alias string - pkg string - typ string + alias string + pkg string + typ string + param string + rmvParam bool } type replaceTypeItem struct { @@ -1105,6 +1160,14 @@ func parseReplaceType(t string) *replaceType { ret.alias = r[0] t = r[1] } + + // Match type parameter substitution + match := regexp.MustCompile(`\[(.*?)\]$`).FindStringSubmatch(t) + if len(match) >= 2 { + ret.param, ret.rmvParam = strings.CutPrefix(match[1], "-") + t = strings.ReplaceAll(t, match[0], "") + } + lastDot := strings.LastIndex(t, ".") lastSlash := strings.LastIndex(t, "/") if lastDot == -1 || (lastSlash > -1 && lastDot < lastSlash) { diff --git a/pkg/generator_test.go b/pkg/generator_test.go index 283337fb..012a9856 100644 --- a/pkg/generator_test.go +++ b/pkg/generator_test.go @@ -799,6 +799,14 @@ func TestParseReplaceType(t *testing.T) { value: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz", expected: replaceType{alias: "", pkg: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz", typ: ""}, }, + { + value: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz/internal/foo.InternalBaz[T]", + expected: replaceType{alias: "", pkg: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz/internal/foo", typ: "InternalBaz", param: "T"}, + }, + { + value: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz/internal/foo.InternalBaz[-T]", + expected: replaceType{alias: "", pkg: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz/internal/foo", typ: "InternalBaz", param: "T", rmvParam: true}, + }, } for _, test := range tests {