Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions mockgen/internal/tests/exclude/ignore/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mockgen/internal/tests/exclude/interfaces.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down
20 changes: 17 additions & 3 deletions mockgen/mockgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
39 changes: 32 additions & 7 deletions mockgen/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -59,6 +87,7 @@ func sourceMode(source string) (*model.Package, error) {
importedInterfaces: newInterfaceCache(),
auxInterfaces: newInterfaceCache(),
srcDir: srcDir,
wantIface: wantIface,
}

// Handle -imports.
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we error out here if wantIface was never found in the file? I don't think we're currently erroring out when we specify a nonexistent interface name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that we should do that.
I didn't originally do that to retain existing behavior, but I have found it annoying/confusing when the mock doesn't get generated.
Archive mode also suffers from this, I think.

For now, I'm going to convert this PR to draft in favor of #200 since that was first, and the author has updated it.

continue
}
i, err := p.parseInterface(ni.name.String(), importPath, ni)
Expand Down
18 changes: 17 additions & 1 deletion mockgen/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}
Loading