From c43c8bcd66d0da3f32bba2595ab98284e006d317 Mon Sep 17 00:00:00 2001 From: Iurii Date: Wed, 14 Oct 2026 10:30:24 +0300 Subject: [PATCH 1/2] fix: resolve embedded interface aliases in source mode --- .../internal/tests/embedded_alias/aliases.go | 7 + .../internal/tests/embedded_alias/input.go | 10 + .../tests/embedded_alias/input_test.go | 22 ++ .../tests/embedded_alias/mock_test.go | 68 ++++++ .../tests/embedded_alias/other/aliases.go | 7 + mockgen/parse.go | 41 +++- mockgen/parse_alias_test.go | 222 ++++++++++++++++++ 7 files changed, 371 insertions(+), 6 deletions(-) create mode 100644 mockgen/internal/tests/embedded_alias/aliases.go create mode 100644 mockgen/internal/tests/embedded_alias/input.go create mode 100644 mockgen/internal/tests/embedded_alias/input_test.go create mode 100644 mockgen/internal/tests/embedded_alias/mock_test.go create mode 100644 mockgen/internal/tests/embedded_alias/other/aliases.go create mode 100644 mockgen/parse_alias_test.go diff --git a/mockgen/internal/tests/embedded_alias/aliases.go b/mockgen/internal/tests/embedded_alias/aliases.go new file mode 100644 index 0000000..2b7469f --- /dev/null +++ b/mockgen/internal/tests/embedded_alias/aliases.go @@ -0,0 +1,7 @@ +package embedded_alias + +type Getter[T any] interface { + Get() T +} + +type LocalAlias = Getter[int] diff --git a/mockgen/internal/tests/embedded_alias/input.go b/mockgen/internal/tests/embedded_alias/input.go new file mode 100644 index 0000000..e786359 --- /dev/null +++ b/mockgen/internal/tests/embedded_alias/input.go @@ -0,0 +1,10 @@ +package embedded_alias + +import "go.uber.org/mock/mockgen/internal/tests/embedded_alias/other" + +//go:generate mockgen -source=input.go -destination=mock_test.go -package=embedded_alias + +type Example interface { + LocalAlias + other.Alias +} diff --git a/mockgen/internal/tests/embedded_alias/input_test.go b/mockgen/internal/tests/embedded_alias/input_test.go new file mode 100644 index 0000000..b35bd96 --- /dev/null +++ b/mockgen/internal/tests/embedded_alias/input_test.go @@ -0,0 +1,22 @@ +package embedded_alias + +import ( + "testing" + + "go.uber.org/mock/gomock" +) + +var _ Example = (*MockExample)(nil) + +func TestEmbeddedAliases(t *testing.T) { + m := NewMockExample(gomock.NewController(t)) + m.EXPECT().Get().Return(42) + m.EXPECT().Map("key").Return(true) + + if got := m.Get(); got != 42 { + t.Errorf("Get() = %d, want 42", got) + } + if !m.Map("key") { + t.Error("Map() = false, want true") + } +} diff --git a/mockgen/internal/tests/embedded_alias/mock_test.go b/mockgen/internal/tests/embedded_alias/mock_test.go new file mode 100644 index 0000000..b61be35 --- /dev/null +++ b/mockgen/internal/tests/embedded_alias/mock_test.go @@ -0,0 +1,68 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: input.go +// +// Generated by this command: +// +// mockgen -source=input.go -destination=mock_test.go -package=embedded_alias +// + +// Package embedded_alias is a generated GoMock package. +package embedded_alias + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockExample is a mock of Example interface. +type MockExample struct { + ctrl *gomock.Controller + recorder *MockExampleMockRecorder + isgomock struct{} +} + +// MockExampleMockRecorder is the mock recorder for MockExample. +type MockExampleMockRecorder struct { + mock *MockExample +} + +// NewMockExample creates a new mock instance. +func NewMockExample(ctrl *gomock.Controller) *MockExample { + mock := &MockExample{ctrl: ctrl} + mock.recorder = &MockExampleMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExample) EXPECT() *MockExampleMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockExample) Get() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get") + ret0, _ := ret[0].(int) + return ret0 +} + +// Get indicates an expected call of Get. +func (mr *MockExampleMockRecorder) Get() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockExample)(nil).Get)) +} + +// Map mocks base method. +func (m *MockExample) Map(arg0 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Map", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Map indicates an expected call of Map. +func (mr *MockExampleMockRecorder) Map(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Map", reflect.TypeOf((*MockExample)(nil).Map), arg0) +} diff --git a/mockgen/internal/tests/embedded_alias/other/aliases.go b/mockgen/internal/tests/embedded_alias/other/aliases.go new file mode 100644 index 0000000..a50f481 --- /dev/null +++ b/mockgen/internal/tests/embedded_alias/other/aliases.go @@ -0,0 +1,7 @@ +package other + +type Mapper[K, V any] interface { + Map(K) V +} + +type Alias = Mapper[string, bool] diff --git a/mockgen/parse.go b/mockgen/parse.go index da0cc5e..4a4f45f 100644 --- a/mockgen/parse.go +++ b/mockgen/parse.go @@ -215,7 +215,7 @@ func (p *fileParser) parseAuxFiles(auxFiles string) error { } func (p *fileParser) addAuxInterfacesFromFile(pkg string, file *ast.File) { - for ni := range iterInterfaces(file) { + for ni := range iterInterfaces(file, true) { p.auxInterfaces.Set(pkg, ni.name.Name, ni) } } @@ -242,7 +242,7 @@ func (p *fileParser) parseFile(importPath string, file *ast.File) (*model.Packag } var is []*model.Interface - for ni := range iterInterfaces(file) { + for ni := range iterInterfaces(file, false) { name := ni.name.String() if _, ok := p.excludeNamesSet[name]; ok { @@ -296,7 +296,7 @@ func (p *fileParser) parsePackage(path string) (*fileParser, error) { for _, pkg := range pkgs { file := ast.MergePackageFiles(pkg, ast.FilterFuncDuplicates|ast.FilterUnassociatedComments|ast.FilterImportDuplicates) - for ni := range iterInterfaces(file) { + for ni := range iterInterfaces(file, true) { newP.importedInterfaces.Set(path, ni.name.Name, ni) } imports, _ := importsOfFile(file) @@ -355,6 +355,12 @@ func (p *fileParser) constructTps(it *namedInterface) (tps map[string]model.Type // parseInterface loads interface specified by pkg and name, parses it and returns // a new model with the parsed. func (p *fileParser) parseInterface(name, pkg string, it *namedInterface) (*model.Interface, error) { + if it.inProgress { + return nil, p.errorf(it.name.Pos(), "cyclic interface embedding involving %s", name) + } + it.inProgress = true + defer func() { it.inProgress = false }() + iface := &model.Interface{Name: name} tps := p.constructTps(it) tp, err := p.parseFieldList(pkg, it.typeParams, tps) @@ -766,10 +772,13 @@ type namedInterface struct { typeParams []*ast.Field embeddedInstTypeParams []ast.Expr instTypes []model.Type + inProgress bool } -// Create an iterator over all interfaces in file. -func iterInterfaces(file *ast.File) <-chan *namedInterface { +// iterInterfaces returns interface declarations. When includeAliases is true, +// aliases are also available for resolving embedded interfaces, but should not +// be used to discover additional interfaces to mock. +func iterInterfaces(file *ast.File, includeAliases bool) <-chan *namedInterface { ch := make(chan *namedInterface) go func() { for _, decl := range file.Decls { @@ -784,7 +793,27 @@ func iterInterfaces(file *ast.File) <-chan *namedInterface { } it, ok := ts.Type.(*ast.InterfaceType) if !ok { - continue + if !includeAliases || !ts.Assign.IsValid() { + continue + } + typ := ts.Type + for { + paren, ok := typ.(*ast.ParenExpr) + if !ok { + break + } + typ = paren.X + } + switch typ.(type) { + case *ast.Ident, *ast.SelectorExpr, *ast.IndexExpr, *ast.IndexListExpr: + // An alias has the same methods as its target. Resolve it + // using the existing embedded-interface parsing logic. + it = &ast.InterfaceType{Methods: &ast.FieldList{ + List: []*ast.Field{{Type: typ}}, + }} + default: + continue + } } ch <- &namedInterface{name: ts.Name, it: it, typeParams: getTypeSpecTypeParams(ts)} diff --git a/mockgen/parse_alias_test.go b/mockgen/parse_alias_test.go new file mode 100644 index 0000000..09f526e --- /dev/null +++ b/mockgen/parse_alias_test.go @@ -0,0 +1,222 @@ +package main + +import ( + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestParseFile_EmbeddedAliases(t *testing.T) { + tests := []struct { + name string + source string + inputs []string + output string + }{ + { + name: "instantiated interface", + source: ` +type Base[T any] interface { Get() T } +type Alias = Base[int] +type Target interface { Alias } +`, + output: "int", + }, + { + name: "multiple type arguments", + source: ` +type Base[K, V any] interface { Get(K) V } +type Alias = Base[int, string] +type Target interface { Alias } +`, + inputs: []string{"int"}, + output: "string", + }, + { + name: "forward alias chain", + source: ` +type Target interface { Chained } +type Chained = Alias +type Alias = Base[int] +type Base[T any] interface { Get() T } +`, + output: "int", + }, + { + name: "parenthesized alias", + source: ` +type Base[T any] interface { Get() T } +type Alias = (Base[int]) +type Target interface { Alias } +`, + output: "int", + }, + { + name: "generic alias", + source: ` +type Base[T any] interface { Get() T } +type Alias[T any] = Base[[]T] +type Target interface { Alias[int] } +`, + output: "[]int", + }, + { + name: "ordinary interface", + source: ` +type Base interface { Get() string } +type Alias = Base +type Target interface { Alias } +`, + output: "string", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := token.NewFileSet() + file, err := parser.ParseFile(fs, "input.go", "package example\n"+tt.source, 0) + if err != nil { + t.Fatal(err) + } + p := fileParser{ + fileSet: fs, + imports: make(map[string]importedPackage), + importedInterfaces: newInterfaceCache(), + auxInterfaces: newInterfaceCache(), + } + p.addAuxInterfacesFromFile("example", file) + pkg, err := p.parseFile("example", file) + if err != nil { + t.Fatal(err) + } + target := -1 + for i, iface := range pkg.Interfaces { + if iface.Name == "Target" { + target = i + } + } + if target < 0 { + t.Fatal("Target interface not found") + } + methods := pkg.Interfaces[target].Methods + if len(methods) != 1 || methods[0].Name != "Get" { + t.Fatalf("expected Get method, got %v", methods) + } + m := methods[0] + if len(m.In) != len(tt.inputs) { + t.Fatalf("got %d inputs, want %d", len(m.In), len(tt.inputs)) + } + for i, want := range tt.inputs { + if got := m.In[i].Type.String(nil, "example"); got != want { + t.Errorf("input %d = %s, want %s", i, got, want) + } + } + if len(m.Out) != 1 { + t.Fatalf("got %d outputs, want 1", len(m.Out)) + } + if got := m.Out[0].Type.String(nil, "example"); got != tt.output { + t.Errorf("output = %s, want %s", got, tt.output) + } + }) + } +} + +func TestParseFile_EmbeddedAliasAuxFiles(t *testing.T) { + auxFile := filepath.Join(t.TempDir(), "aliases.go") + if err := os.WriteFile(auxFile, []byte(`package dep +type Base[T any] interface { Get() T } +type Alias = Base[int] +`), 0o600); err != nil { + t.Fatal(err) + } + fs := token.NewFileSet() + file, err := parser.ParseFile(fs, "input.go", `package example +import dep "example.test/dep" +type Target interface { dep.Alias } +`, 0) + if err != nil { + t.Fatal(err) + } + p := fileParser{ + fileSet: fs, + imports: make(map[string]importedPackage), + importedInterfaces: newInterfaceCache(), + auxInterfaces: newInterfaceCache(), + } + if err := p.parseAuxFiles("dep=" + auxFile); err != nil { + t.Fatal(err) + } + pkg, err := p.parseFile("example", file) + if err != nil { + t.Fatal(err) + } + if len(pkg.Interfaces) != 1 || len(pkg.Interfaces[0].Methods) != 1 { + t.Fatalf("expected one interface with one method, got %v", pkg.Interfaces) + } + if got := pkg.Interfaces[0].Methods[0].Out[0].Type.String(nil, "example"); got != "int" { + t.Errorf("output = %s, want int", got) + } +} + +func TestParseFile_EmbeddedAliasDiscovery(t *testing.T) { + fs := token.NewFileSet() + file, err := parser.ParseFile(fs, "input.go", `package example +type Base[T any] interface { Get() T } +type Alias = Base[int] +type Number = int +type Slice = []int +type Data[T any] struct { Value T } +type DataAlias = Data[int] +type First interface { Alias } +type Second interface { Base[string] } +type Third interface { Alias } +`, 0) + if err != nil { + t.Fatal(err) + } + p := fileParser{ + fileSet: fs, + imports: make(map[string]importedPackage), + importedInterfaces: newInterfaceCache(), + auxInterfaces: newInterfaceCache(), + } + p.addAuxInterfacesFromFile("example", file) + pkg, err := p.parseFile("example", file) + if err != nil { + t.Fatal(err) + } + // Aliases must be available for embedding without becoming extra mocks. + if len(pkg.Interfaces) != 4 { + t.Fatalf("got %d interfaces, want 4", len(pkg.Interfaces)) + } + for i, want := range []string{"T", "int", "string", "int"} { + if got := pkg.Interfaces[i].Methods[0].Out[0].Type.String(nil, "example"); got != want { + t.Errorf("interface %s output = %s, want %s", pkg.Interfaces[i].Name, got, want) + } + } +} + +func TestParseFile_EmbeddedAliasCycle(t *testing.T) { + fs := token.NewFileSet() + file, err := parser.ParseFile(fs, "input.go", `package example +type A = B +type B = A +type Target interface { A } +`, 0) + if err != nil { + t.Fatal(err) + } + p := fileParser{ + fileSet: fs, + imports: make(map[string]importedPackage), + importedInterfaces: newInterfaceCache(), + auxInterfaces: newInterfaceCache(), + } + p.addAuxInterfacesFromFile("example", file) + _, err = p.parseFile("example", file) + if err == nil || !strings.Contains(err.Error(), "cyclic") { + t.Fatalf("got error %v, want a cyclic embedding error", err) + } +} From 5b3cb0c9d148bbddbd69014e8c00a5c361be184c Mon Sep 17 00:00:00 2001 From: Iurii Date: Wed, 14 Oct 2026 10:45:12 +0300 Subject: [PATCH 2/2] test: cover embedded generic callbacks in package mode --- .../tests/embedded_generic_callback/input.go | 16 ++++++ .../embedded_generic_callback/input_test.go | 24 +++++++++ .../embedded_generic_callback/mock_test.go | 52 +++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 mockgen/internal/tests/embedded_generic_callback/input.go create mode 100644 mockgen/internal/tests/embedded_generic_callback/input_test.go create mode 100644 mockgen/internal/tests/embedded_generic_callback/mock_test.go diff --git a/mockgen/internal/tests/embedded_generic_callback/input.go b/mockgen/internal/tests/embedded_generic_callback/input.go new file mode 100644 index 0000000..018d6a3 --- /dev/null +++ b/mockgen/internal/tests/embedded_generic_callback/input.go @@ -0,0 +1,16 @@ +package mypackage + +//go:generate mockgen -destination=mock_test.go -package=mypackage go.uber.org/mock/mockgen/internal/tests/embedded_generic_callback MyInterface + +// Regression case from https://github.com/uber-go/mock/issues/271#issuecomment-3442087301. +type Callback[T any] func(item T) + +type Base[T any] interface { + OnEvent(callback Callback[T]) +} + +type MyStruct struct{} + +type MyInterface interface { + Base[MyStruct] +} diff --git a/mockgen/internal/tests/embedded_generic_callback/input_test.go b/mockgen/internal/tests/embedded_generic_callback/input_test.go new file mode 100644 index 0000000..41f6ac1 --- /dev/null +++ b/mockgen/internal/tests/embedded_generic_callback/input_test.go @@ -0,0 +1,24 @@ +package mypackage + +import ( + "testing" + + "go.uber.org/mock/gomock" +) + +func TestEmbeddedGenericCallback(t *testing.T) { + m := NewMockMyInterface(gomock.NewController(t)) + m.EXPECT().OnEvent(gomock.Any()).Do(func(callback Callback[MyStruct]) { + callback(MyStruct{}) + }) + + // The generated method must accept Callback[MyStruct] from this package. + var target MyInterface = m + called := false + target.OnEvent(func(MyStruct) { + called = true + }) + if !called { + t.Fatal("OnEvent did not invoke the callback") + } +} diff --git a/mockgen/internal/tests/embedded_generic_callback/mock_test.go b/mockgen/internal/tests/embedded_generic_callback/mock_test.go new file mode 100644 index 0000000..171c702 --- /dev/null +++ b/mockgen/internal/tests/embedded_generic_callback/mock_test.go @@ -0,0 +1,52 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: go.uber.org/mock/mockgen/internal/tests/embedded_generic_callback (interfaces: MyInterface) +// +// Generated by this command: +// +// mockgen -destination=mock_test.go -package=mypackage go.uber.org/mock/mockgen/internal/tests/embedded_generic_callback MyInterface +// + +// Package mypackage is a generated GoMock package. +package mypackage + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockMyInterface is a mock of MyInterface interface. +type MockMyInterface struct { + ctrl *gomock.Controller + recorder *MockMyInterfaceMockRecorder + isgomock struct{} +} + +// MockMyInterfaceMockRecorder is the mock recorder for MockMyInterface. +type MockMyInterfaceMockRecorder struct { + mock *MockMyInterface +} + +// NewMockMyInterface creates a new mock instance. +func NewMockMyInterface(ctrl *gomock.Controller) *MockMyInterface { + mock := &MockMyInterface{ctrl: ctrl} + mock.recorder = &MockMyInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMyInterface) EXPECT() *MockMyInterfaceMockRecorder { + return m.recorder +} + +// OnEvent mocks base method. +func (m *MockMyInterface) OnEvent(callback Callback[MyStruct]) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "OnEvent", callback) +} + +// OnEvent indicates an expected call of OnEvent. +func (mr *MockMyInterfaceMockRecorder) OnEvent(callback any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnEvent", reflect.TypeOf((*MockMyInterface)(nil).OnEvent), callback) +}