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
78 changes: 57 additions & 21 deletions formatting/enable/gofumpt/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"go/ast"
"go/token"
"strings"
"sync"

"github.com/go-openapi/codegen/formatting/internal/rules"
Expand All @@ -32,24 +33,52 @@ func apply(fset *token.FileSet, file *ast.File) {
fumpt.File(fset, file, current)
}

// Option configures the gofumpt rules.
type Option func(*fumpt.Options) error
type (
// Option configures the gofumpt rules.
Option func(options) options

// options carries the gofumpt settings being assembled, and the first option that rejected its
// arguments. [fumpt.Options] holds no pointer, so the copy the chain passes along is a value.
options struct {
fumpt fumpt.Options
err error
}
)

// withError keeps the first failure and lets the rest of the chain run.
func (o options) withError(err error) options {
if o.err == nil {
o.err = err
}

return o
}

// applyWithDefaults folds the chain over the zero options, left to right.
//
// The zero value is gofumpt's own default: every extra rule off, and a language version of go1.
func applyWithDefaults(opts []Option) options {
var o options

for _, apply := range opts {
o = apply(o)
}

return o
}

// Configure sets the rules for the whole program.
//
// Call it once, before formatting anything. It returns an error when an option is not one gofumpt
// knows, and leaves the previous settings in place.
func Configure(opts ...Option) error {
next := fumpt.Options{}

for _, apply := range opts {
if err := apply(&next); err != nil {
return err
}
next := applyWithDefaults(opts)
if next.err != nil {
return next.err
}

mx.Lock()
settings = next
settings = next.fumpt
mx.Unlock()

return nil
Expand All @@ -60,10 +89,10 @@ func Configure(opts ...Option) error {
// gofumpt holds back the rules that need a language newer than the code targets. Empty means
// go1, which holds back all of them.
func WithLangVersion(version string) Option {
return func(o *fumpt.Options) error {
o.LangVersion = version
return func(o options) options {
o.fumpt.LangVersion = version

return nil
return o
}
}

Expand All @@ -72,23 +101,30 @@ func WithLangVersion(version string) Option {
// gofumpt reads it to decide which import paths are outside the standard library when it puts the
// standard library imports first.
func WithModulePath(path string) Option {
return func(o *fumpt.Options) error {
o.ModulePath = path
return func(o options) options {
o.fumpt.ModulePath = path

return nil
return o
}
}

// WithExtraRules turns on rules gofumpt leaves off, named as gofumpt names them on its command line:
// "group_params", "clothe_returns", "balance_calls". Passing "true" turns all of them on.
//
// The rules named replace whatever an earlier WithExtraRules asked for, which is how gofumpt's own
// -extra flag behaves. Name every rule in one call:
//
// gofumpt.WithExtraRules("group_params", "clothe_returns")
func WithExtraRules(rules ...string) Option {
return func(o *fumpt.Options) error {
for _, rule := range rules {
if err := o.Extra.Set(rule); err != nil {
return fmt.Errorf("unknown gofumpt rule %q: %w", rule, err)
}
return func(o options) options {
// Extra.Set clears itself before reading a list, so one call per rule would keep only the
// last. It takes the comma-separated form gofumpt's -extra flag takes.
named := strings.Join(rules, ",")

if err := o.fumpt.Extra.Set(named); err != nil {
return o.withError(fmt.Errorf("unknown gofumpt rule in %q: %w", named, err))
}

return nil
return o
}
}
21 changes: 21 additions & 0 deletions formatting/enable/gofumpt/enable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,27 @@ func TestEnable(t *testing.T) {
assert.Contains(t, format(t, source(t, "generated"), formatting.WithGoFumpt()), "func F(a, b int)")
})

t.Run("should apply every rule named in one call", func(t *testing.T) {
// gofumpt's Extra.Set clears itself before reading a list, so asking for the rules one Set at
// a time kept only the last of them.
require.NoError(t, gofumpt.Configure(
gofumpt.WithLangVersion("go1.25"),
gofumpt.WithExtraRules("group_params", "clothe_returns"),
))

out := format(t, source(t, "generated"), formatting.WithGoFumpt())

assert.Contains(t, out, "func F(a, b int)",
"group_params is named first, and the rule after it must not clear it")
})

t.Run("should name the rule it does not know", func(t *testing.T) {
err := gofumpt.Configure(gofumpt.WithExtraRules("group_params", "no_such_rule"))

require.Error(t, err)
assert.Contains(t, err.Error(), "no_such_rule")
})

t.Run("should leave the parameters alone without the extra rule", func(t *testing.T) {
require.NoError(t, gofumpt.Configure(gofumpt.WithLangVersion("go1.25")))

Expand Down
2 changes: 1 addition & 1 deletion formatting/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func sourceBytes[T Source](src T) []byte {
}

func format(w io.Writer, src []byte, opts ...Option) (*ImportsReport, error) {
o := optionsWithDefaults(opts)
o := applyWithDefaults(opts)

var extraRules rules.Func
if o.goFumpt {
Expand Down
60 changes: 39 additions & 21 deletions formatting/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,37 @@

package formatting

// Option configures [Format].
type Option func(*options)

type options struct {
groups []string
goFumpt bool
forcePruning bool
simplifyAliases bool
resolved map[string]string
}
import "maps"

type (
// Option configures [Format].
Option func(options) options

options struct {
groups []string
resolved map[string]string

goFumpt bool
forcePruning bool
simplifyAliases bool
}
)

// WithImportGroups adds one import group per prefix, between the standard library and the rest.
//
// An import belongs to the first prefix it starts with, so pass the more specific prefix first.
// Without this option the output has two groups: the standard library, then everything else.
func WithImportGroups(prefixes ...string) Option {
return func(o *options) {
return func(o options) options {
for _, prefix := range prefixes {
if prefix == "" {
continue
}

o.groups = append(o.groups, prefix)
}

return o
}
}

Expand All @@ -34,8 +42,10 @@ func WithImportGroups(prefixes ...string) Option {
// Blank-import github.com/go-openapi/codegen/formatting/enable/gofumpt to make the rules available.
// Without it [Format] returns [ErrNoGoFumpt] rather than printing without them.
func WithGoFumpt() Option {
return func(o *options) {
return func(o options) options {
o.goFumpt = true

return o
}
}

Expand All @@ -60,8 +70,10 @@ func WithGoFumpt() Option {
// }),
// )
func WithForceImportsPruning() Option {
return func(o *options) {
return func(o options) options {
o.forcePruning = true

return o
}
}

Expand All @@ -80,18 +92,18 @@ func WithForceImportsPruning() Option {
// github.com/go-openapi/codegen/formatting/resolve, which answers from the packages themselves rather
// than from the machine, so one map serves every build.
func WithResolvedImports(names map[string]string) Option {
return func(o *options) {
return func(o options) options {
if len(names) == 0 {
return
return o
}

if o.resolved == nil {
o.resolved = make(map[string]string, len(names))
}

for importPath, name := range names {
o.resolved[importPath] = name
}
maps.Copy(o.resolved, names)

return o
}
}

Expand All @@ -112,16 +124,22 @@ func WithResolvedImports(names map[string]string) Option {
//
// Nothing is dropped on a guess. Without evidence from the table or the map, every alias stays.
func WithSimplifiedImportAliases() Option {
return func(o *options) {
return func(o options) options {
o.simplifyAliases = true

return o
}
}

func optionsWithDefaults(opts []Option) options {
// applyWithDefaults folds the chain over the zero options, left to right.
//
// The zero value is the default throughout: no group past the standard library and the rest, no
// gofumpt, no forced pruning, and no name the caller supplied.
func applyWithDefaults(opts []Option) options {
var o options

for _, apply := range opts {
apply(&o)
o = apply(o)
}

return o
Expand Down
49 changes: 34 additions & 15 deletions formatting/resolve/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,39 +26,47 @@ const (
ErrUnresolved Error = "some import paths did not resolve"
)

// Option configures [Names].
type Option func(*options)

type options struct {
dir string
env []string
buildFlags []string
}
type (
// Option configures [Names].
Option func(options) options

options struct {
dir string
env []string
buildFlags []string
}
)

// WithDir loads the packages as if from dir.
//
// "go list" runs there, so dir has to sit in a module requiring the paths being asked about. Without
// it the current working directory is used, which is right only when the process already runs inside
// that module.
func WithDir(dir string) Option {
return func(o *options) {
return func(o options) options {
o.dir = dir

return o
}
}

// WithEnv replaces the environment "go list" runs with, in the form os.Environ returns.
//
// Use it to pin GOFLAGS, GOPATH or GOMODCACHE. An empty slice leaves the process environment alone.
func WithEnv(env []string) Option {
return func(o *options) {
return func(o options) options {
o.env = slices.Clone(env)

return o
}
}

// WithBuildFlags passes flags to "go list", as in -tags or -mod=mod.
func WithBuildFlags(flags ...string) Option {
return func(o *options) {
return func(o options) options {
o.buildFlags = append(o.buildFlags, flags...)

return o
}
}

Expand All @@ -85,10 +93,7 @@ func Names(ctx context.Context, paths []string, opts ...Option) (map[string]stri
return map[string]string{}, nil
}

var o options
for _, apply := range opts {
apply(&o)
}
o := applyWithDefaults(opts)

loaded, err := packages.Load(&packages.Config{
Context: ctx,
Expand Down Expand Up @@ -172,3 +177,17 @@ func missingFrom(wanted []string, names, reasons map[string]string) []string {
func oneLine(message string) string {
return strings.Join(strings.Fields(message), " ")
}

// applyWithDefaults folds the chain over the zero options, left to right.
//
// The zero value is the default: go list runs in the working directory, with the process environment
// and no build flags.
func applyWithDefaults(opts []Option) options {
var o options

for _, apply := range opts {
o = apply(o)
}

return o
}
2 changes: 1 addition & 1 deletion genapp/genapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type GoGenApp struct {
// [github.com/go-openapi/codegen/templates-repo.New], and a repository that would not build has
// already reported why by the time it reaches here.
func New(opts ...Option) (*GoGenApp, error) {
o := optionsWithDefaults(opts)
o := applyWithDefaults(opts)

if o.templates == nil {
return nil, fmt.Errorf("a templates repository is required, see WithTemplates: %w", ErrGenApp)
Expand Down
2 changes: 1 addition & 1 deletion genapp/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const goModFile = "go.mod"
// A go.mod already in the output path is left alone and reported as [fs.ErrExist], unless
// [WithReplaceExisting] says otherwise.
func (g *GoGenApp) InitModule(opts ...ModOption) error {
o, err := modOptionsWithDefaults(opts)
o, err := applyModWithDefaults(opts)
if err != nil {
return err
}
Expand Down
Loading
Loading