From 907b46be2fa23d3e1d0d14a47ca4f346b7f9419e Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Thu, 2 Oct 2025 10:27:31 -0700 Subject: [PATCH] source mode: support specifying interfaces Adds an optional positional argument in -source mode to specify a comma-separated list of interfaces to generate mocks for. Resolves #140 --- CHANGELOG.md | 4 +- README.md | 9 +++- mockgen/internal/tests/exclude/ignore/mock.go | 54 +++++++++++++++++++ mockgen/internal/tests/exclude/interfaces.go | 1 + mockgen/mockgen.go | 20 +++++-- mockgen/parse.go | 39 +++++++++++--- mockgen/parse_test.go | 18 ++++++- 7 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 mockgen/internal/tests/exclude/ignore/mock.go diff --git a/CHANGELOG.md b/CHANGELOG.md index cb99ed8d..69e3fa5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## Unreleased -- No changes yet. +### Added +- Source mode: support for specifying a subset of interfaces to mock + via a comma-separated list argument. ## 0.6.0 (18 Aug 2025) ### Added diff --git a/README.md b/README.md index 4ab94d8d..943daa45 100644 --- a/README.md +++ b/README.md @@ -61,13 +61,18 @@ mockgen -archive=pkg.a database/sql/driver Conn,Driver ### Source mode Source mode generates mock interfaces from a source file. -It is enabled by using the -source flag. Other flags that -may be useful in this mode are -imports and -aux_files. +It is enabled by using the -source flag. +By default, it generates mocks for all interfaces in the file. +You can specify a comma-separated list of interfaces to generate +using a single non-flag argument. +Other flags that may be useful in this mode are -imports, +-aux_files and -exclude_interfaces. Example: ```bash mockgen -source=foo.go [other options] +mockgen -source=foo.go [other options] SomeInterface,OtherInterface ``` ### Package mode diff --git a/mockgen/internal/tests/exclude/ignore/mock.go b/mockgen/internal/tests/exclude/ignore/mock.go new file mode 100644 index 00000000..ca8af5f1 --- /dev/null +++ b/mockgen/internal/tests/exclude/ignore/mock.go @@ -0,0 +1,54 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interfaces.go +// +// Generated by this command: +// +// mockgen -source=interfaces.go -destination=ignore/mock.go IgnoreMe +// + +// Package mock_exclude is a generated GoMock package. +package mock_exclude + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockIgnoreMe is a mock of IgnoreMe interface. +type MockIgnoreMe struct { + ctrl *gomock.Controller + recorder *MockIgnoreMeMockRecorder + isgomock struct{} +} + +// MockIgnoreMeMockRecorder is the mock recorder for MockIgnoreMe. +type MockIgnoreMeMockRecorder struct { + mock *MockIgnoreMe +} + +// NewMockIgnoreMe creates a new mock instance. +func NewMockIgnoreMe(ctrl *gomock.Controller) *MockIgnoreMe { + mock := &MockIgnoreMe{ctrl: ctrl} + mock.recorder = &MockIgnoreMeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIgnoreMe) EXPECT() *MockIgnoreMeMockRecorder { + return m.recorder +} + +// A mocks base method. +func (m *MockIgnoreMe) A() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "A") + ret0, _ := ret[0].(bool) + return ret0 +} + +// A indicates an expected call of A. +func (mr *MockIgnoreMeMockRecorder) A() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "A", reflect.TypeOf((*MockIgnoreMe)(nil).A)) +} diff --git a/mockgen/internal/tests/exclude/interfaces.go b/mockgen/internal/tests/exclude/interfaces.go index 37bacf1b..557c990d 100644 --- a/mockgen/internal/tests/exclude/interfaces.go +++ b/mockgen/internal/tests/exclude/interfaces.go @@ -1,6 +1,7 @@ package exclude //go:generate mockgen -source=interfaces.go -destination=mock.go -package=exclude -exclude_interfaces=IgnoreMe,IgnoreMe2 +//go:generate mockgen -source=interfaces.go -destination=ignore/mock.go IgnoreMe type IgnoreMe interface { A() bool diff --git a/mockgen/mockgen.go b/mockgen/mockgen.go index d3d92ba1..256281c0 100644 --- a/mockgen/mockgen.go +++ b/mockgen/mockgen.go @@ -95,7 +95,16 @@ func main() { case *modelGob != "": // gob mode pkg, err = gobMode(*modelGob) case *source != "": // source mode - pkg, err = sourceMode(*source) + if flag.NArg() > 1 { + usage() + log.Fatal("Expected at most one argument with -source") + } + var ifaces []string + if flag.NArg() == 1 { + ifaces = strings.Split(flag.Arg(0), ",") + } + + pkg, err = sourceMode(*source, ifaces) case *archive != "": // archive mode checkArgs() packageName = flag.Arg(0) @@ -256,10 +265,15 @@ func usage() { const usageText = `mockgen has three modes of operation: archive, source and package. Source mode generates mock interfaces from a source file. -It is enabled by using the -source flag. Other flags that -may be useful in this mode are -imports, -aux_files and -exclude_interfaces. +It is enabled by using the -source flag. +By default, it generates mocks for all interfaces in the file. +You can specify a comma-separated list of interfaces to generate +using a single non-flag argument. +Other flags that may be useful in this mode are -imports, -aux_files and +-exclude_interfaces. Example: mockgen -source=foo.go [other options] + mockgen -source=foo.go [other options] SomeInterface,OtherInterface Package mode works by specifying the package and interface names. It is enabled by passing two non-flag arguments: an import path, and a diff --git a/mockgen/parse.go b/mockgen/parse.go index f43321c3..c924614f 100644 --- a/mockgen/parse.go +++ b/mockgen/parse.go @@ -36,7 +36,35 @@ import ( ) // sourceMode generates mocks via source file. -func sourceMode(source string) (*model.Package, error) { +// +// ifaces is a list of interface names to generate mocks for. +// If nil or empty, all interfaces in the source file are used. +func sourceMode(source string, ifaces []string) (*model.Package, error) { + var wantIface func(name string) bool + if len(ifaces) == 0 { + wantIface = func(name string) bool { return true } + } else { + wantIfaces := make(map[string]struct{}) + for _, n := range ifaces { + wantIfaces[n] = struct{}{} + } + wantIface = func(name string) bool { + _, ok := wantIfaces[name] + return ok + } + } + + if *excludeInterfaces != "" { + oldWantIface := wantIface + excludeNamesSet := parseExcludeInterfaces(*excludeInterfaces) + wantIface = func(name string) bool { + if _, ok := excludeNamesSet[name]; ok { + return false + } + return oldWantIface(name) + } + } + srcDir, err := filepath.Abs(filepath.Dir(source)) if err != nil { return nil, fmt.Errorf("failed getting source directory: %v", err) @@ -59,6 +87,7 @@ func sourceMode(source string) (*model.Package, error) { importedInterfaces: newInterfaceCache(), auxInterfaces: newInterfaceCache(), srcDir: srcDir, + wantIface: wantIface, } // Handle -imports. @@ -75,10 +104,6 @@ func sourceMode(source string) (*model.Package, error) { } } - if *excludeInterfaces != "" { - p.excludeNamesSet = parseExcludeInterfaces(*excludeInterfaces) - } - // Handle -aux_files. if err := p.parseAuxFiles(*auxFiles); err != nil { return nil, err @@ -167,7 +192,7 @@ type fileParser struct { auxFiles []*ast.File auxInterfaces *interfaceCache srcDir string - excludeNamesSet map[string]struct{} + wantIface func(name string) bool } func (p *fileParser) errorf(pos token.Pos, format string, args ...any) error { @@ -228,7 +253,7 @@ func (p *fileParser) parseFile(importPath string, file *ast.File) (*model.Packag var is []*model.Interface for ni := range iterInterfaces(file) { - if _, ok := p.excludeNamesSet[ni.name.String()]; ok { + if p.wantIface != nil && !p.wantIface(ni.name.String()) { continue } i, err := p.parseInterface(ni.name.String(), importPath, ni) diff --git a/mockgen/parse_test.go b/mockgen/parse_test.go index 3c4ba4cf..e85c57e4 100644 --- a/mockgen/parse_test.go +++ b/mockgen/parse_test.go @@ -109,7 +109,7 @@ func checkGreeterImports(t *testing.T, imports map[string]importedPackage) { func Benchmark_parseFile(b *testing.B) { source := "internal/tests/performance/big_interface/big_interface.go" for n := 0; n < b.N; n++ { - sourceMode(source) + sourceMode(source, nil) } } @@ -143,3 +143,19 @@ func TestParseArrayWithConstLength(t *testing.T) { } } } + +func TestSourceMode_interfaceSubset(t *testing.T) { + pkg, err := sourceMode("internal/tests/exclude/interfaces.go", []string{"IgnoreMe"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if want, got := 1, len(pkg.Interfaces); want != got { + t.Fatalf("Expected %d interfaces but got %d", want, got) + } + + iface := pkg.Interfaces[0] + if want, got := "IgnoreMe", iface.Name; want != got { + t.Fatalf("Expected interface name to be %s but got %s", want, got) + } +}