Skip to content
Merged
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
13 changes: 12 additions & 1 deletion .mockery_testify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,15 @@ packages:
Foo:
pkg-path: github.com/vektra/mockery/v3/internal/fixtures/replace_type_pointers
type-name: Bar

github.com/vektra/mockery/v3/internal/fixtures/directive_comments:
config:
all: False
interfaces:
MatryerRequester:
config:
structname: TheMatryerRequester
InterfaceWithoutGenerate:
ServerWithDifferentFile:
configs:
- structname: FunServerWithDifferentFile
- structname: AnotherFunServerWithDifferentFile
109 changes: 97 additions & 12 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// package config defines the schemas and functionality of the .mockery.yml
// Package config defines the schemas and functionality of the .mockery.yml
// config files. This package is NOT meant to be used by external Go libraries.
// We expose the contents of this package purely for documentation purposes.
//
Expand Down Expand Up @@ -34,6 +34,7 @@ import (
"github.com/vektra/mockery/v3/internal/stackerr"
"github.com/vektra/mockery/v3/template_funcs"
"golang.org/x/tools/go/packages"
"gopkg.in/yaml.v3"
)

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

func (c PackageConfig) GetInterfaceConfig(ctx context.Context, interfaceName string) *InterfaceConfig {
log := zerolog.Ctx(ctx)
func (c PackageConfig) GetInterfaceConfig(ctx context.Context, interfaceName string, directiveConfig *Config) (*InterfaceConfig, error) {
// If the interface has an explicit config, override it with the directive config.
// This favor any config set in the directive comment over the original file based config.
if ifaceConfig, ok := c.Interfaces[interfaceName]; ok {
return ifaceConfig
if directiveConfig != nil {
newConfig, err := deep.Copy(directiveConfig)
if err != nil {
return nil, fmt.Errorf("cloning directive config: %w", err)
}

// Merge the interface config into the directive config clone.
mergeConfigs(ctx, *ifaceConfig.Config, newConfig)

ifaceConfig.Config = newConfig

for i, subCfg := range ifaceConfig.Configs {
newConfig, err := deep.Copy(directiveConfig)
if err != nil {
return nil, fmt.Errorf("cloning directive config: %w", err)
}

// Merge the interface config into the directive config clone.
mergeConfigs(ctx, *subCfg, newConfig)
ifaceConfig.Configs[i] = newConfig
}
}

return ifaceConfig, nil
}

// We don't have a specific config for this interface,
// we should create a new one.
ifaceConfig := NewInterfaceConfig()

newConfig, err := deep.Copy(c.Config)
if err != nil {
log.Err(err).Msg("issue when deep-copying package config to interface config")
panic(err)
// If there is a directive config, use it as the base config.
if directiveConfig != nil {
newConfig, err := deep.Copy(directiveConfig)
if err != nil {
return nil, fmt.Errorf("cloning directive config: %w", err)
}
ifaceConfig.Config = newConfig
}

ifaceConfig.Config = newConfig
ifaceConfig.Configs = []*Config{newConfig}
return ifaceConfig
// Finally, merge the package config into the new config
mergeConfigs(ctx, *c.Config, ifaceConfig.Config)

ifaceConfig.Configs = []*Config{ifaceConfig.Config}
return ifaceConfig, nil
}

func (c PackageConfig) ShouldGenerateInterface(ctx context.Context, interfaceName string) (bool, error) {
func (c PackageConfig) ShouldGenerateInterface(
ctx context.Context,
interfaceName string,
ifaceConfig Config,
hasDirectiveComment bool,
) (bool, error) {
log := zerolog.Ctx(ctx)
if hasDirectiveComment {
if ifaceConfig.Generate != nil && !*ifaceConfig.Generate {
log.Debug().Msg("interface has directive comment with generate: false, skipping generation")
return false, nil
}
log.Debug().Msg("interface has directive comment, generating mock")
return true, nil
}
if *c.Config.All {
if *c.Config.IncludeInterfaceRegex != "" {
log.Warn().Msg("interface config has both `all` and `include-interface-regex` set: `include-interface-regex` will be ignored")
Expand Down Expand Up @@ -526,6 +573,7 @@ type Config struct {
// ForceFileWrite controls whether mockery will overwrite existing files when generating mocks. This is by default set to false.
ForceFileWrite *bool `koanf:"force-file-write" yaml:"force-file-write,omitempty"`
Formatter *string `koanf:"formatter" yaml:"formatter,omitempty"`
Generate *bool `koanf:"generate" yaml:"generate,omitempty"`
IncludeAutoGenerated *bool `koanf:"include-auto-generated" yaml:"include-auto-generated,omitempty"`
IncludeInterfaceRegex *string `koanf:"include-interface-regex" yaml:"include-interface-regex,omitempty"`
InPackage *bool `koanf:"inpackage" yaml:"inpackage,omitempty"`
Expand Down Expand Up @@ -695,3 +743,40 @@ func (c *Config) GetReplacement(pkgPath string, typeName string) *ReplaceType {
}
return pkgMap[typeName]
}

// ExtractDirectiveConfig parses interface's documentation from a declaration
// node and extracts mockery's directive configuration.
//
// Mockery directives are comments that start with "mockery:" and can appear
// multiple times in the interface's doc comments. All such comments are combined
// and interpreted as YAML configuration.
func ExtractDirectiveConfig(ctx context.Context, decl *ast.GenDecl) (*Config, error) {
if decl == nil || decl.Doc == nil {
return nil, nil
}

var yamlConfig []string

// Extract all mockery directive comments and build a YAML document
for _, doc := range decl.Doc.List {
// Look for directive comments `//mockery:<config-key>: <value>` and convert them to YAML
if value, found := strings.CutPrefix(doc.Text, "//mockery:"); found && value != "" {
yamlConfig = append(yamlConfig, value)
}
}
Comment on lines +758 to +766

Choose a reason for hiding this comment

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

I can see that this is an easy and convenient way to translate into mockery config, but it might be nice to have some alternative syntax, rather than spreading yaml through multiple directive lines, especially if you could take advantage of the provided ast.ParseDirective function?

Something like this might be nicer, but I appreciate it may be a bit more work...

//mockery:generate -structname MyMock -filename my_mock.go

Copy link
Member

Choose a reason for hiding this comment

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

I err on the side of being obvious versus being clever. Emulating command line args with mockery actually having any command line args is confusing and weird. I'm okay with the way this is currently implemented.


if len(yamlConfig) == 0 {
return nil, nil
}

// Combine all YAML lines into a single document
yamlDoc := strings.Join(yamlConfig, "\n")

// Parse the YAML directly into the directiveConfig struct
directiveConfig := Config{}
if err := yaml.Unmarshal([]byte(yamlDoc), &directiveConfig); err != nil {
return nil, fmt.Errorf("unmarshaling directive yaml: %w", err)
}

return &directiveConfig, nil
}
103 changes: 103 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"go/ast"
"os"
"path"
"testing"
Expand Down Expand Up @@ -71,3 +72,105 @@ packages:
_, _, err := NewRootConfig(context.Background(), flags)
assert.NoError(t, err)
}

func TestExtractConfigFromDirectiveComments(t *testing.T) {
configs := []struct {
name string
commentLines []string
expected *Config
expectError bool
}{
{
name: "no directive comments",
commentLines: []string{
"// This is a regular comment.",
"// Another regular comment.",
},
expected: nil,
expectError: false,
},
{
name: "regular comments are not directive comments",
commentLines: []string{
"// Directive comments *must* shouldn't have spaces after the slashes.",
"// mockery:structname: MyMock",
},
expected: nil,
expectError: false,
},
{
name: "valid single-line directive comment",
commentLines: []string{
"//mockery:structname: MyMock",
},
expected: &Config{
StructName: ptr("MyMock"),
},
},
{
name: "valid multi-line directive comments",
commentLines: []string{
"// Some initial comment.",
"//mockery:structname: MyMock",
"//mockery:filename: my_mock.go",
"// Some trailing comment.",
},
expected: &Config{
StructName: ptr("MyMock"),
FileName: ptr("my_mock.go"),
},
expectError: false,
},
{
name: "invalid directive comment format",
commentLines: []string{
"//mockery:structname MyMock", // Missing ':'
},
expected: nil,
expectError: true,
},
{
name: "unsupported configuration key are ignored",
commentLines: []string{
"//mockery:unknown_key: value",
},
expected: &Config{},
expectError: false,
},
{
name: "mixed valid and invalid directive comments",
commentLines: []string{
"//mockery:structname: MyMock",
"//mockery:invalid_format", // Invalid
"//mockery:filename: my_mock.go",
},
expected: nil,
expectError: true,
},
}

for _, tt := range configs {
t.Run(tt.name, func(t *testing.T) {
comments := make([]*ast.Comment, len(tt.commentLines))
for i, line := range tt.commentLines {
comments[i] = &ast.Comment{Text: line}
}

result, err := ExtractDirectiveConfig(context.Background(), &ast.GenDecl{
Doc: &ast.CommentGroup{
List: comments,
},
})
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}

func ptr[T any](s T) *T {
return &s
}
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Parameter Descriptions
| `filename` | :fontawesome-solid-check: | `#!yaml "mocks_test.go"` | The name of the file the mock will reside in. |
| `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. |
| `formatter` | :fontawesome-solid-x: | `#!yaml "goimports"` | The formatter to use on the rendered template. Choices are: `gofmt`, `goimports`, `noop`. |
| `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. |
| [`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. |
| `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`. |
| [`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. |
Expand Down
53 changes: 53 additions & 0 deletions docs/generate-directive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
## Description

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:

```yaml title=".mockery.yml"
all: false
packages:
github.com/vektra/mockery/v3/internal/fixtures/directive_comments_example:
```

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:

```go title="interface.go"
package directivecommentsexample

// Requester is an interface that defines a method for making HTTP requests.
//
//mockery:generate: true
type Requester interface {
Get(path string) (string, error)
}
```

<div class="result" markdown>
``` title=""
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
```
</div>

We can also specify any config value that mockery supports. For example, let's rename the mock's structname:

```
// Requester is an interface that defines a method for making HTTP requests.
//
//mockery:generate: true
//mockery:structname: MockFoo
type Requester interface {
Get(path string) (string, error)
}
```

The new `structname` is applied as expected:

```go
// MockFoo is an autogenerated mock type for the Requester type
type MockFoo struct {
mock.Mock
}
```

!!! note

The `#!yaml generate:` parameter is only effectual from within the doc comment itself. It has no effect if specified within the mockery config file.
1 change: 1 addition & 0 deletions internal/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dir: '{{.InterfaceDir}}'
filename: mocks_test.go
force-file-write: true
formatter: goimports
generate: true
include-auto-generated: false
log-level: info
structname: '{{.Mock}}{{.InterfaceName}}'
Expand Down
Loading