From 7842e1d5c37edf34abddb9b6e1f3f35685f91774 Mon Sep 17 00:00:00 2001 From: "Hoonmin Kim (Micah)" Date: Mon, 25 Sep 2023 09:00:21 +0900 Subject: [PATCH] proposal: ignore interfaces that contains type constraints (#93) This is a proposal PR to generate mocks without error for following cases: ```go type Water[R any, C UnsignedInteger] interface { Fish(R) []C } type UnsignedInteger interface { ~uint | ~uint32 | ~uint64 } ``` Go `types` package seems to wrap interfaces that contain type constraints by checking a type set literal of the form `~T` and `A|B`. https://github.com/golang/go/blob/master/src/go/types/decl.go#L668 So gomock can just ignore that pattern to generate mocks safely without `don't know how to mock method of type` errors. I think this can be a better solution for custom type constraints than [`-exclude` flag](https://github.com/uber-go/mock/pull/72). --------- Co-authored-by: Jacob Oaks --- mockgen/generic_go118.go | 14 ++++ mockgen/internal/tests/generics/generics.go | 8 +++ .../generics/source/mock_generics_mock.go | 66 ++++++++++++------- mockgen/parse.go | 3 + 4 files changed, 67 insertions(+), 24 deletions(-) diff --git a/mockgen/generic_go118.go b/mockgen/generic_go118.go index 635402d..b7b4494 100644 --- a/mockgen/generic_go118.go +++ b/mockgen/generic_go118.go @@ -11,8 +11,10 @@ package main import ( + "errors" "fmt" "go/ast" + "go/token" "strings" "go.uber.org/mock/mockgen/model" @@ -98,6 +100,16 @@ func (p *fileParser) parseGenericMethod(field *ast.Field, it *namedInterface, if case *ast.IndexListExpr: indices = v.Indices typ = v.X + case *ast.UnaryExpr: + if v.Op == token.TILDE { + return nil, errConstraintInterface + } + return nil, fmt.Errorf("~T may only appear as constraint for %T", field.Type) + case *ast.BinaryExpr: + if v.Op == token.OR { + return nil, errConstraintInterface + } + return nil, fmt.Errorf("A|B may only appear as constraint for %T", field.Type) default: return nil, fmt.Errorf("don't know how to mock method of type %T", field.Type) } @@ -114,3 +126,5 @@ func (p *fileParser) parseGenericMethod(field *ast.Field, it *namedInterface, if return p.parseMethod(nf, it, iface, pkg, tps) } + +var errConstraintInterface = errors.New("interface contains constraints") diff --git a/mockgen/internal/tests/generics/generics.go b/mockgen/internal/tests/generics/generics.go index 85d1f0d..db0c49e 100644 --- a/mockgen/internal/tests/generics/generics.go +++ b/mockgen/internal/tests/generics/generics.go @@ -57,3 +57,11 @@ type SolarSystem[T constraints.Ordered] interface { type Earth[R any] interface { Water(R) []R } + +type Water[R any, C UnsignedInteger] interface { + Fish(R) []C +} + +type UnsignedInteger interface { + ~uint | ~uint32 | ~uint64 +} diff --git a/mockgen/internal/tests/generics/source/mock_generics_mock.go b/mockgen/internal/tests/generics/source/mock_generics_mock.go index 08c1e00..d92a8d4 100644 --- a/mockgen/internal/tests/generics/source/mock_generics_mock.go +++ b/mockgen/internal/tests/generics/source/mock_generics_mock.go @@ -1,6 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: generics.go - +// +// Generated by this command: +// +// mockgen --source=generics.go --destination=source/mock_generics_mock.go --package source +// // Package source is a generated GoMock package. package source @@ -306,29 +310,6 @@ func (mr *MockBarMockRecorder[T, R]) Two(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Two", reflect.TypeOf((*MockBar[T, R])(nil).Two), arg0) } -// MockIface is a mock of Iface interface. -type MockIface[T any] struct { - ctrl *gomock.Controller - recorder *MockIfaceMockRecorder[T] -} - -// MockIfaceMockRecorder is the mock recorder for MockIface. -type MockIfaceMockRecorder[T any] struct { - mock *MockIface[T] -} - -// NewMockIface creates a new mock instance. -func NewMockIface[T any](ctrl *gomock.Controller) *MockIface[T] { - mock := &MockIface[T]{ctrl: ctrl} - mock.recorder = &MockIfaceMockRecorder[T]{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockIface[T]) EXPECT() *MockIfaceMockRecorder[T] { - return m.recorder -} - // MockUniverse is a mock of Universe interface. type MockUniverse[T constraints.Signed] struct { ctrl *gomock.Controller @@ -476,3 +457,40 @@ func (mr *MockEarthMockRecorder[R]) Water(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Water", reflect.TypeOf((*MockEarth[R])(nil).Water), arg0) } + +// MockWater is a mock of Water interface. +type MockWater[R any, C generics.UnsignedInteger] struct { + ctrl *gomock.Controller + recorder *MockWaterMockRecorder[R, C] +} + +// MockWaterMockRecorder is the mock recorder for MockWater. +type MockWaterMockRecorder[R any, C generics.UnsignedInteger] struct { + mock *MockWater[R, C] +} + +// NewMockWater creates a new mock instance. +func NewMockWater[R any, C generics.UnsignedInteger](ctrl *gomock.Controller) *MockWater[R, C] { + mock := &MockWater[R, C]{ctrl: ctrl} + mock.recorder = &MockWaterMockRecorder[R, C]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWater[R, C]) EXPECT() *MockWaterMockRecorder[R, C] { + return m.recorder +} + +// Fish mocks base method. +func (m *MockWater[R, C]) Fish(arg0 R) []C { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Fish", arg0) + ret0, _ := ret[0].([]C) + return ret0 +} + +// Fish indicates an expected call of Fish. +func (mr *MockWaterMockRecorder[R, C]) Fish(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fish", reflect.TypeOf((*MockWater[R, C])(nil).Fish), arg0) +} diff --git a/mockgen/parse.go b/mockgen/parse.go index 9521409..f43321c 100644 --- a/mockgen/parse.go +++ b/mockgen/parse.go @@ -232,6 +232,9 @@ func (p *fileParser) parseFile(importPath string, file *ast.File) (*model.Packag continue } i, err := p.parseInterface(ni.name.String(), importPath, ni) + if errors.Is(err, errConstraintInterface) { + continue + } if err != nil { return nil, err }