Skip to content

Commit f3eb94f

Browse files
paivagustavoLandonTClipp
authored andcommitted
Add support for overriding configuration through comments on interfaces
Add docs and examples Bump minor version
1 parent 390f232 commit f3eb94f

15 files changed

+860
-24
lines changed

.mockery_testify.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,15 @@ packages:
127127
Foo:
128128
pkg-path: github.com/vektra/mockery/v3/internal/fixtures/replace_type_pointers
129129
type-name: Bar
130-
130+
github.com/vektra/mockery/v3/internal/fixtures/directive_comments:
131+
config:
132+
all: False
133+
interfaces:
134+
MatryerRequester:
135+
config:
136+
structname: TheMatryerRequester
137+
InterfaceWithoutGenerate:
138+
ServerWithDifferentFile:
139+
configs:
140+
- structname: FunServerWithDifferentFile
141+
- structname: AnotherFunServerWithDifferentFile

config/config.go

Lines changed: 97 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// package config defines the schemas and functionality of the .mockery.yml
1+
// Package config defines the schemas and functionality of the .mockery.yml
22
// config files. This package is NOT meant to be used by external Go libraries.
33
// We expose the contents of this package purely for documentation purposes.
44
//
@@ -34,6 +34,7 @@ import (
3434
"github.com/vektra/mockery/v3/internal/stackerr"
3535
"github.com/vektra/mockery/v3/template_funcs"
3636
"golang.org/x/tools/go/packages"
37+
"gopkg.in/yaml.v3"
3738
)
3839

3940
// TemplateData is the data sent to the template for the config file.
@@ -72,6 +73,7 @@ func NewDefaultKoanf(ctx context.Context) (*koanf.Koanf, error) {
7273
FileName: addr("mocks_test.go"),
7374
ForceFileWrite: addr(true),
7475
Formatter: addr("goimports"),
76+
Generate: addr(true),
7577
IncludeAutoGenerated: addr(false),
7678
LogLevel: addr("info"),
7779
StructName: addr("{{.Mock}}{{.InterfaceName}}"),
@@ -418,26 +420,71 @@ func (c *PackageConfig) Initialize(ctx context.Context) error {
418420
return nil
419421
}
420422

421-
func (c PackageConfig) GetInterfaceConfig(ctx context.Context, interfaceName string) *InterfaceConfig {
422-
log := zerolog.Ctx(ctx)
423+
func (c PackageConfig) GetInterfaceConfig(ctx context.Context, interfaceName string, directiveConfig *Config) (*InterfaceConfig, error) {
424+
// If the interface has an explicit config, override it with the directive config.
425+
// This favor any config set in the directive comment over the original file based config.
423426
if ifaceConfig, ok := c.Interfaces[interfaceName]; ok {
424-
return ifaceConfig
427+
if directiveConfig != nil {
428+
newConfig, err := deep.Copy(directiveConfig)
429+
if err != nil {
430+
return nil, fmt.Errorf("cloning directive config: %w", err)
431+
}
432+
433+
// Merge the interface config into the directive config clone.
434+
mergeConfigs(ctx, *ifaceConfig.Config, newConfig)
435+
436+
ifaceConfig.Config = newConfig
437+
438+
for i, subCfg := range ifaceConfig.Configs {
439+
newConfig, err := deep.Copy(directiveConfig)
440+
if err != nil {
441+
return nil, fmt.Errorf("cloning directive config: %w", err)
442+
}
443+
444+
// Merge the interface config into the directive config clone.
445+
mergeConfigs(ctx, *subCfg, newConfig)
446+
ifaceConfig.Configs[i] = newConfig
447+
}
448+
}
449+
450+
return ifaceConfig, nil
425451
}
452+
453+
// We don't have a specific config for this interface,
454+
// we should create a new one.
426455
ifaceConfig := NewInterfaceConfig()
427456

428-
newConfig, err := deep.Copy(c.Config)
429-
if err != nil {
430-
log.Err(err).Msg("issue when deep-copying package config to interface config")
431-
panic(err)
457+
// If there is a directive config, use it as the base config.
458+
if directiveConfig != nil {
459+
newConfig, err := deep.Copy(directiveConfig)
460+
if err != nil {
461+
return nil, fmt.Errorf("cloning directive config: %w", err)
462+
}
463+
ifaceConfig.Config = newConfig
432464
}
433465

434-
ifaceConfig.Config = newConfig
435-
ifaceConfig.Configs = []*Config{newConfig}
436-
return ifaceConfig
466+
// Finally, merge the package config into the new config
467+
mergeConfigs(ctx, *c.Config, ifaceConfig.Config)
468+
469+
ifaceConfig.Configs = []*Config{ifaceConfig.Config}
470+
return ifaceConfig, nil
437471
}
438472

439-
func (c PackageConfig) ShouldGenerateInterface(ctx context.Context, interfaceName string) (bool, error) {
473+
func (c PackageConfig) ShouldGenerateInterface(
474+
ctx context.Context,
475+
interfaceName string,
476+
ifaceConfig Config,
477+
hasDirectiveComment bool,
478+
) (bool, error) {
440479
log := zerolog.Ctx(ctx)
480+
if hasDirectiveComment {
481+
if ifaceConfig.Generate != nil && !*ifaceConfig.Generate {
482+
log.Debug().Msg("interface has directive comment with generate: false, skipping generation")
483+
return false, nil
484+
}
485+
log.Debug().Msg("interface has directive comment, generating mock")
486+
return true, nil
487+
}
441488
if *c.Config.All {
442489
if *c.Config.IncludeInterfaceRegex != "" {
443490
log.Warn().Msg("interface config has both `all` and `include-interface-regex` set: `include-interface-regex` will be ignored")
@@ -526,6 +573,7 @@ type Config struct {
526573
// ForceFileWrite controls whether mockery will overwrite existing files when generating mocks. This is by default set to false.
527574
ForceFileWrite *bool `koanf:"force-file-write" yaml:"force-file-write,omitempty"`
528575
Formatter *string `koanf:"formatter" yaml:"formatter,omitempty"`
576+
Generate *bool `koanf:"generate" yaml:"generate,omitempty"`
529577
IncludeAutoGenerated *bool `koanf:"include-auto-generated" yaml:"include-auto-generated,omitempty"`
530578
IncludeInterfaceRegex *string `koanf:"include-interface-regex" yaml:"include-interface-regex,omitempty"`
531579
InPackage *bool `koanf:"inpackage" yaml:"inpackage,omitempty"`
@@ -695,3 +743,40 @@ func (c *Config) GetReplacement(pkgPath string, typeName string) *ReplaceType {
695743
}
696744
return pkgMap[typeName]
697745
}
746+
747+
// ExtractDirectiveConfig parses interface's documentation from a declaration
748+
// node and extracts mockery's directive configuration.
749+
//
750+
// Mockery directives are comments that start with "mockery:" and can appear
751+
// multiple times in the interface's doc comments. All such comments are combined
752+
// and interpreted as YAML configuration.
753+
func ExtractDirectiveConfig(ctx context.Context, decl *ast.GenDecl) (*Config, error) {
754+
if decl == nil || decl.Doc == nil {
755+
return nil, nil
756+
}
757+
758+
var yamlConfig []string
759+
760+
// Extract all mockery directive comments and build a YAML document
761+
for _, doc := range decl.Doc.List {
762+
// Look for directive comments `//mockery:<config-key>: <value>` and convert them to YAML
763+
if value, found := strings.CutPrefix(doc.Text, "//mockery:"); found && value != "" {
764+
yamlConfig = append(yamlConfig, value)
765+
}
766+
}
767+
768+
if len(yamlConfig) == 0 {
769+
return nil, nil
770+
}
771+
772+
// Combine all YAML lines into a single document
773+
yamlDoc := strings.Join(yamlConfig, "\n")
774+
775+
// Parse the YAML directly into the directiveConfig struct
776+
directiveConfig := Config{}
777+
if err := yaml.Unmarshal([]byte(yamlDoc), &directiveConfig); err != nil {
778+
return nil, fmt.Errorf("unmarshaling directive yaml: %w", err)
779+
}
780+
781+
return &directiveConfig, nil
782+
}

config/config_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"go/ast"
78
"os"
89
"path"
910
"testing"
@@ -71,3 +72,105 @@ packages:
7172
_, _, err := NewRootConfig(context.Background(), flags)
7273
assert.NoError(t, err)
7374
}
75+
76+
func TestExtractConfigFromDirectiveComments(t *testing.T) {
77+
configs := []struct {
78+
name string
79+
commentLines []string
80+
expected *Config
81+
expectError bool
82+
}{
83+
{
84+
name: "no directive comments",
85+
commentLines: []string{
86+
"// This is a regular comment.",
87+
"// Another regular comment.",
88+
},
89+
expected: nil,
90+
expectError: false,
91+
},
92+
{
93+
name: "regular comments are not directive comments",
94+
commentLines: []string{
95+
"// Directive comments *must* shouldn't have spaces after the slashes.",
96+
"// mockery:structname: MyMock",
97+
},
98+
expected: nil,
99+
expectError: false,
100+
},
101+
{
102+
name: "valid single-line directive comment",
103+
commentLines: []string{
104+
"//mockery:structname: MyMock",
105+
},
106+
expected: &Config{
107+
StructName: ptr("MyMock"),
108+
},
109+
},
110+
{
111+
name: "valid multi-line directive comments",
112+
commentLines: []string{
113+
"// Some initial comment.",
114+
"//mockery:structname: MyMock",
115+
"//mockery:filename: my_mock.go",
116+
"// Some trailing comment.",
117+
},
118+
expected: &Config{
119+
StructName: ptr("MyMock"),
120+
FileName: ptr("my_mock.go"),
121+
},
122+
expectError: false,
123+
},
124+
{
125+
name: "invalid directive comment format",
126+
commentLines: []string{
127+
"//mockery:structname MyMock", // Missing ':'
128+
},
129+
expected: nil,
130+
expectError: true,
131+
},
132+
{
133+
name: "unsupported configuration key are ignored",
134+
commentLines: []string{
135+
"//mockery:unknown_key: value",
136+
},
137+
expected: &Config{},
138+
expectError: false,
139+
},
140+
{
141+
name: "mixed valid and invalid directive comments",
142+
commentLines: []string{
143+
"//mockery:structname: MyMock",
144+
"//mockery:invalid_format", // Invalid
145+
"//mockery:filename: my_mock.go",
146+
},
147+
expected: nil,
148+
expectError: true,
149+
},
150+
}
151+
152+
for _, tt := range configs {
153+
t.Run(tt.name, func(t *testing.T) {
154+
comments := make([]*ast.Comment, len(tt.commentLines))
155+
for i, line := range tt.commentLines {
156+
comments[i] = &ast.Comment{Text: line}
157+
}
158+
159+
result, err := ExtractDirectiveConfig(context.Background(), &ast.GenDecl{
160+
Doc: &ast.CommentGroup{
161+
List: comments,
162+
},
163+
})
164+
if tt.expectError {
165+
require.Error(t, err)
166+
} else {
167+
require.NoError(t, err)
168+
assert.Equal(t, tt.expected, result)
169+
}
170+
})
171+
}
172+
}
173+
174+
func ptr[T any](s T) *T {
175+
return &s
176+
}

docs/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ Parameter Descriptions
125125
| `filename` | :fontawesome-solid-check: | `#!yaml "mocks_test.go"` | The name of the file the mock will reside in. |
126126
| `force-file-write` | :fontawesome-solid-x: | `#!yaml true` | When set to `#!yaml force-file-write: true`, mockery will forcibly overwrite any existing files. Otherwise, it will fail if the output file already exists. |
127127
| `formatter` | :fontawesome-solid-x: | `#!yaml "goimports"` | The formatter to use on the rendered template. Choices are: `gofmt`, `goimports`, `noop`. |
128+
| `generate` | :fontawesome-solid-x: | `#!yaml true` | Can be used to selectively enable/disable generation of specific interfaces. See [the related docs](generate-directive.md) for more details. |
128129
| [`include-auto-generated`](include-auto-generated.md){ data-preview } | :fontawesome-solid-x: | `#!yaml false` | When set to `true`, mockery will parse files that are auto-generated. This can only be specified in the top-level config or package-level config. |
129130
| `include-interface-regex` | :fontawesome-solid-x: | `#!yaml ""` | When set, only interface names that match the expression will be generated. This setting is ignored if `all: True` is specified in the configuration. To further refine the interfaces generated, use `exclude-interface-regex`. |
130131
| [`inpackage`](inpackage.md){ data-preview } | :fontawesome-solid-x: | `#!yaml nil` | When set, this overrides mockery's auto-detection logic when determining if the mock file is inside or outside of the mocked interface's package. |

docs/generate-directive.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
## Description
2+
3+
An alternative way to configure mocks is to use the `#!go //mockery:generate` directive. Mockery parses the doc comments and allows you to override configuration of specific interfaces in the source code. For example:
4+
5+
```yaml title=".mockery.yml"
6+
all: false
7+
packages:
8+
github.com/vektra/mockery/v3/internal/fixtures/directive_comments_example:
9+
```
10+
11+
We set the top-level `#!yaml all: false` (which is the default value, anyway) to ensure that interfaces are by default not generated. We can then specify the doc comment directive to include the interface:
12+
13+
```go title="interface.go"
14+
package directivecommentsexample
15+
16+
// Requester is an interface that defines a method for making HTTP requests.
17+
//
18+
//mockery:generate: true
19+
type Requester interface {
20+
Get(path string) (string, error)
21+
}
22+
```
23+
24+
<div class="result" markdown>
25+
``` title=""
26+
2025-11-11T14:40:22.897665000-05:00 INF adding interface to collection collection=/Users/landonclipp/git/LandonTClipp/mockery/internal/fixtures/directive_comments_example/mocks_test.go interface=Requester package-path=github.com/vektra/mockery/v3/internal/fixtures/directive_comments_example version=v0.0.0-dev
27+
```
28+
</div>
29+
30+
We can also specify any config value that mockery supports. For example, let's rename the mock's structname:
31+
32+
```
33+
// Requester is an interface that defines a method for making HTTP requests.
34+
//
35+
//mockery:generate: true
36+
//mockery:structname: MockFoo
37+
type Requester interface {
38+
Get(path string) (string, error)
39+
}
40+
```
41+
42+
The new `structname` is applied as expected:
43+
44+
```go
45+
// MockFoo is an autogenerated mock type for the Requester type
46+
type MockFoo struct {
47+
mock.Mock
48+
}
49+
```
50+
51+
!!! note
52+
53+
The `#!yaml generate:` parameter is only effectual from within the doc comment itself. It has no effect if specified within the mockery config file.

internal/cmd/init_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dir: '{{.InterfaceDir}}'
1414
filename: mocks_test.go
1515
force-file-write: true
1616
formatter: goimports
17+
generate: true
1718
include-auto-generated: false
1819
log-level: info
1920
structname: '{{.Mock}}{{.InterfaceName}}'

0 commit comments

Comments
 (0)