diff --git a/config/config.go b/config/config.go index b9af4283..be87552f 100644 --- a/config/config.go +++ b/config/config.go @@ -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" + "golang.org/x/tools/imports" "gopkg.in/yaml.v3" ) @@ -65,6 +66,14 @@ func addr[T any](v T) *T { return &v } +func deref[T any](t *T) T { + if t == nil { + return *new(T) + } + + return *t +} + func NewDefaultKoanf(ctx context.Context) (*koanf.Koanf, error) { c := Config{ All: addr(false), @@ -73,6 +82,7 @@ func NewDefaultKoanf(ctx context.Context) (*koanf.Koanf, error) { FileName: addr("mocks_test.go"), ForceFileWrite: addr(true), Formatter: addr("goimports"), + FormatterOptions: defaultFormatterOptions(), Generate: addr(true), IncludeAutoGenerated: addr(false), LogLevel: addr("info"), @@ -91,6 +101,22 @@ func NewDefaultKoanf(ctx context.Context) (*koanf.Koanf, error) { return k, nil } +func defaultFormatterOptions() *FormatterOptions { + return &FormatterOptions{ + // Matches goimports defaults: + // https://cs.opensource.google/go/x/tools/+/refs/tags/v0.41.0:imports/forward.go;l=53 + GoImports: &GoImports{ + AllErrors: addr(false), + Comments: addr(true), + FormatOnly: addr(false), + Fragment: addr(false), + LocalPrefix: addr(""), + TabIndent: addr(true), + TabWidth: addr(8), + }, + } +} + type RootConfig struct { Config `koanf:",squash" yaml:",inline"` Packages map[string]*PackageConfig `koanf:"packages" yaml:"packages"` @@ -561,6 +587,35 @@ type ReplaceType struct { TypeName string `koanf:"type-name" yaml:"type-name,omitempty"` } +type GoImports struct { + AllErrors *bool `koanf:"all-errors" yaml:"all-errors,omitempty"` + Comments *bool `koanf:"comments" yaml:"comments,omitempty"` + FormatOnly *bool `koanf:"format-only" yaml:"format-only,omitempty"` + Fragment *bool `koanf:"fragment" yaml:"fragment,omitempty"` + LocalPrefix *string `koanf:"local-prefix" yaml:"local-prefix,omitempty"` + TabIndent *bool `koanf:"tab-indent" yaml:"tab-indent,omitempty"` + TabWidth *int `koanf:"tab-width" yaml:"tab-width,omitempty"` +} + +func (g *GoImports) GetLocalPrefix() string { + return deref(g.LocalPrefix) +} + +func (g *GoImports) Options() *imports.Options { + return &imports.Options{ + AllErrors: deref(g.AllErrors), + Comments: deref(g.Comments), + FormatOnly: deref(g.FormatOnly), + Fragment: deref(g.Fragment), + TabIndent: deref(g.TabIndent), + TabWidth: deref(g.TabWidth), + } +} + +type FormatterOptions struct { + GoImports *GoImports `koanf:"goimports" yaml:"goimports,omitempty"` +} + type Config struct { All *bool `koanf:"all" yaml:"all,omitempty"` Anchors map[string]any `koanf:"_anchors" yaml:"_anchors,omitempty"` @@ -571,16 +626,17 @@ type Config struct { ExcludeInterfaceRegex *string `koanf:"exclude-interface-regex" yaml:"exclude-interface-regex,omitempty"` FileName *string `koanf:"filename" yaml:"filename,omitempty"` // 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"` - LogLevel *string `koanf:"log-level" yaml:"log-level,omitempty"` - StructName *string `koanf:"structname" yaml:"structname,omitempty"` - PkgName *string `koanf:"pkgname" yaml:"pkgname,omitempty"` - Recursive *bool `koanf:"recursive" yaml:"recursive,omitempty"` + ForceFileWrite *bool `koanf:"force-file-write" yaml:"force-file-write,omitempty"` + Formatter *string `koanf:"formatter" yaml:"formatter,omitempty"` + FormatterOptions *FormatterOptions `koanf:"formatter-options" yaml:"formatter-options,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"` + LogLevel *string `koanf:"log-level" yaml:"log-level,omitempty"` + StructName *string `koanf:"structname" yaml:"structname,omitempty"` + PkgName *string `koanf:"pkgname" yaml:"pkgname,omitempty"` + Recursive *bool `koanf:"recursive" yaml:"recursive,omitempty"` // ReplaceType is a nested map of format map["package path"]["type name"]*ReplaceType ReplaceType map[string]map[string]*ReplaceType `koanf:"replace-type" yaml:"replace-type,omitempty"` // RequireTemplateSchemaExists sets whether mockery will fail if the specified diff --git a/config/config_test.go b/config/config_test.go index da10365d..b22a0165 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -73,6 +73,61 @@ packages: assert.NoError(t, err) } +func TestNewRootConfigDefaultFormatterOptions(t *testing.T) { + configFile := path.Join(t.TempDir(), "config.yaml") + require.NoError(t, os.WriteFile(configFile, []byte(` +formatter: goimports +`), 0o600)) + + flags := pflag.NewFlagSet("test", pflag.ExitOnError) + flags.String("config", "", "") + + require.NoError(t, flags.Parse([]string{"--config", configFile})) + cfg, _, err := NewRootConfig(context.Background(), flags) + require.NoError(t, err) + require.NotNil(t, cfg.FormatterOptions.GoImports) + assert.Equal(t, "goimports", *cfg.Formatter) + assert.False(t, *cfg.FormatterOptions.GoImports.AllErrors) + assert.True(t, *cfg.FormatterOptions.GoImports.Comments) + assert.False(t, *cfg.FormatterOptions.GoImports.FormatOnly) + assert.Equal(t, "", *cfg.FormatterOptions.GoImports.LocalPrefix) + assert.True(t, *cfg.FormatterOptions.GoImports.TabIndent) + assert.Equal(t, 8, *cfg.FormatterOptions.GoImports.TabWidth) +} + +func TestNewRootConfigFormatterOptions(t *testing.T) { + configFile := path.Join(t.TempDir(), "config.yaml") + require.NoError(t, os.WriteFile(configFile, []byte(` +formatter: goimports +formatter-options: + goimports: + all-errors: true + comments: false + format-only: true + fragment: true + local-prefix: github.com/vektra/mockery + tab-indent: false + tab-width: 4 + +`), 0o600)) + + flags := pflag.NewFlagSet("test", pflag.ExitOnError) + flags.String("config", "", "") + + require.NoError(t, flags.Parse([]string{"--config", configFile})) + cfg, _, err := NewRootConfig(context.Background(), flags) + require.NoError(t, err) + require.NotNil(t, cfg.FormatterOptions.GoImports) + assert.Equal(t, "goimports", *cfg.Formatter) + assert.True(t, *cfg.FormatterOptions.GoImports.AllErrors) + assert.False(t, *cfg.FormatterOptions.GoImports.Comments) + assert.True(t, *cfg.FormatterOptions.GoImports.FormatOnly) + assert.True(t, *cfg.FormatterOptions.GoImports.Fragment) + assert.Equal(t, "github.com/vektra/mockery", *cfg.FormatterOptions.GoImports.LocalPrefix) + assert.False(t, *cfg.FormatterOptions.GoImports.TabIndent) + assert.Equal(t, 4, *cfg.FormatterOptions.GoImports.TabWidth) +} + func TestExtractConfigFromDirectiveComments(t *testing.T) { configs := []struct { name string diff --git a/docs/configuration.md b/docs/configuration.md index 67a639bf..e05f30c5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -125,6 +125,7 @@ Parameter Descriptions | `filename` | :fontawesome-solid-check: | `#!yaml "mocks_test.go"` | The name of the file the mock will reside in. Multiple interfaces from the same source package can be placed into the same output file. | | `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`. | +| `formatter-options` | :fontawesome-solid-x: | `#!yaml nil` | Additional options for the formatter. Currently supports `goimports.local-prefix` which will set the corresponding option when running goimports. | | `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`. | diff --git a/internal/template_generator.go b/internal/template_generator.go index 3b441292..8ef62666 100644 --- a/internal/template_generator.go +++ b/internal/template_generator.go @@ -13,6 +13,7 @@ import ( "path/filepath" "strings" + "github.com/davecgh/go-spew/spew" "github.com/rs/zerolog" "github.com/vektra/mockery/v3/config" "github.com/vektra/mockery/v3/internal/file" @@ -179,7 +180,7 @@ func NewTemplateGenerator( func (g *TemplateGenerator) format(src []byte) ([]byte, error) { switch g.formatter { case FormatGoImports: - return goimports(src) + return goimports(src, g.pkgConfig.FormatterOptions.GoImports) case FormatGofmt: return gofmt(src) case FormatNoop: @@ -485,14 +486,13 @@ func (g *TemplateGenerator) Generate( return formatted, nil } -func goimports(src []byte) ([]byte, error) { - formatted, err := imports.Process("/", src, &imports.Options{ - TabWidth: 8, - TabIndent: true, - Comments: true, - Fragment: true, - FormatOnly: true, - }) +// TODO: fix nil pointer if empty formatter-options is provided in config +func goimports(src []byte, opts *config.GoImports) ([]byte, error) { + spew.Dump(opts) + + imports.LocalPrefix = opts.GetLocalPrefix() + + formatted, err := imports.Process("/", src, opts.Options()) if err != nil { return nil, fmt.Errorf("goimports: %s", err) }