-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat: new fmt
command with dedicated formatter configuration
#5357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
d4a00b0
refactor: split processor and matcher
ldez 49ee0ae
feat: formatters configuration structures
ldez d9ce3be
chore: simplify createBuildInfo
ldez b372f01
refactor: use isolate MetaFormatter from linters
ldez 399d55c
feat: new command
ldez a3ab7ff
feat: formatter settings overrides linter settings
ldez e890bb2
feat: propagate formatter exclusions to linter exclusions
ldez 4754086
feat: isolate formatter setting deprecations
ldez ffe0837
feat: add enabled formatters to enabled linters
ldez 6d22c72
review
ldez 42182f0
feat: add diff flag (with exit code)
ldez 492756f
chore: run fmt command
ldez e75afbf
chore: remove golang.org/x/exp/maps
ldez File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: new command
- Loading branch information
commit 399d55c305f9a2833a2d681d62c744c0b1835317
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package commands | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/spf13/viper" | ||
|
||
"github.com/golangci/golangci-lint/pkg/config" | ||
"github.com/golangci/golangci-lint/pkg/goformat" | ||
"github.com/golangci/golangci-lint/pkg/goformatters" | ||
"github.com/golangci/golangci-lint/pkg/logutils" | ||
"github.com/golangci/golangci-lint/pkg/result/processors" | ||
) | ||
|
||
type fmtCommand struct { | ||
viper *viper.Viper | ||
cmd *cobra.Command | ||
|
||
opts config.LoaderOptions | ||
|
||
cfg *config.Config | ||
|
||
buildInfo BuildInfo | ||
|
||
runner *goformat.Runner | ||
|
||
log logutils.Log | ||
debugf logutils.DebugFunc | ||
} | ||
|
||
func newFmtCommand(logger logutils.Log, info BuildInfo) *fmtCommand { | ||
c := &fmtCommand{ | ||
viper: viper.New(), | ||
log: logger, | ||
debugf: logutils.Debug(logutils.DebugKeyExec), | ||
cfg: config.NewDefault(), | ||
buildInfo: info, | ||
} | ||
|
||
fmtCmd := &cobra.Command{ | ||
Use: "fmt", | ||
Short: "Format Go source files", | ||
RunE: c.execute, | ||
PreRunE: c.preRunE, | ||
PersistentPreRunE: c.persistentPreRunE, | ||
SilenceUsage: true, | ||
} | ||
|
||
fmtCmd.SetOut(logutils.StdOut) // use custom output to properly color it in Windows terminals | ||
fmtCmd.SetErr(logutils.StdErr) | ||
|
||
flagSet := fmtCmd.Flags() | ||
flagSet.SortFlags = false // sort them as they are defined here | ||
|
||
setupConfigFileFlagSet(flagSet, &c.opts) | ||
|
||
setupFormattersFlagSet(c.viper, flagSet) | ||
|
||
c.cmd = fmtCmd | ||
|
||
return c | ||
} | ||
|
||
func (c *fmtCommand) persistentPreRunE(cmd *cobra.Command, args []string) error { | ||
c.log.Infof("%s", c.buildInfo.String()) | ||
|
||
loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), c.opts, c.cfg, args) | ||
|
||
err := loader.Load(config.LoadOptions{CheckDeprecation: true, Validation: true}) | ||
if err != nil { | ||
return fmt.Errorf("can't load config: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (c *fmtCommand) preRunE(_ *cobra.Command, _ []string) error { | ||
metaFormatter, err := goformatters.NewMetaFormatter(c.log, &c.cfg.Formatters, &c.cfg.Run) | ||
if err != nil { | ||
return fmt.Errorf("failed to create meta-formatter: %w", err) | ||
} | ||
|
||
matcher := processors.NewGeneratedFileMatcher(c.cfg.Formatters.Exclusions.Generated) | ||
|
||
opts, err := goformat.NewRunnerOptions(c.cfg) | ||
if err != nil { | ||
return fmt.Errorf("build walk options: %w", err) | ||
} | ||
|
||
c.runner = goformat.NewRunner(c.log, metaFormatter, matcher, opts) | ||
|
||
return nil | ||
} | ||
|
||
func (c *fmtCommand) execute(_ *cobra.Command, args []string) error { | ||
if !logutils.HaveDebugTag(logutils.DebugKeyLintersOutput) { | ||
// Don't allow linters and loader to print anything | ||
log.SetOutput(io.Discard) | ||
savedStdout, savedStderr := c.setOutputToDevNull() | ||
defer func() { | ||
os.Stdout, os.Stderr = savedStdout, savedStderr | ||
}() | ||
} | ||
|
||
paths, err := cleanArgs(args) | ||
if err != nil { | ||
return fmt.Errorf("failed to clean arguments: %w", err) | ||
} | ||
|
||
c.log.Infof("Formatting Go files...") | ||
|
||
err = c.runner.Run(paths) | ||
if err != nil { | ||
return fmt.Errorf("failed to process files: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (c *fmtCommand) setOutputToDevNull() (savedStdout, savedStderr *os.File) { | ||
savedStdout, savedStderr = os.Stdout, os.Stderr | ||
devNull, err := os.Open(os.DevNull) | ||
if err != nil { | ||
c.log.Warnf("Can't open null device %q: %s", os.DevNull, err) | ||
return | ||
} | ||
|
||
os.Stdout, os.Stderr = devNull, devNull | ||
return | ||
} | ||
|
||
func cleanArgs(args []string) ([]string, error) { | ||
if len(args) == 0 { | ||
abs, err := filepath.Abs(".") | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return []string{abs}, nil | ||
} | ||
|
||
var expended []string | ||
ldez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for _, arg := range args { | ||
abs, err := filepath.Abs(strings.ReplaceAll(arg, "...", "")) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
expended = append(expended, abs) | ||
} | ||
|
||
return expended, nil | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package goformat | ||
ldez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"fmt" | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/golangci/golangci-lint/pkg/config" | ||
"github.com/golangci/golangci-lint/pkg/fsutils" | ||
"github.com/golangci/golangci-lint/pkg/goformatters" | ||
"github.com/golangci/golangci-lint/pkg/logutils" | ||
"github.com/golangci/golangci-lint/pkg/result/processors" | ||
) | ||
|
||
type Runner struct { | ||
log logutils.Log | ||
|
||
metaFormatter *goformatters.MetaFormatter | ||
matcher *processors.GeneratedFileMatcher | ||
|
||
opts RunnerOptions | ||
} | ||
|
||
func NewRunner(log logutils.Log, | ||
metaFormatter *goformatters.MetaFormatter, matcher *processors.GeneratedFileMatcher, | ||
opts RunnerOptions) *Runner { | ||
return &Runner{ | ||
log: log, | ||
matcher: matcher, | ||
metaFormatter: metaFormatter, | ||
opts: opts, | ||
} | ||
} | ||
|
||
func (c *Runner) Run(paths []string) error { | ||
for _, path := range paths { | ||
err := c.walk(path) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (c *Runner) walk(root string) error { | ||
return filepath.Walk(root, func(path string, f fs.FileInfo, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if f.IsDir() && skipDir(f.Name()) { | ||
return fs.SkipDir | ||
} | ||
|
||
// Ignore non-Go files. | ||
ldez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if !isGoFile(f) { | ||
return nil | ||
} | ||
|
||
match, err := c.opts.MatchPatterns(path) | ||
if err != nil || match { | ||
return err | ||
} | ||
|
||
input, err := os.ReadFile(path) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
match, err = c.matcher.IsGeneratedFile(path, input) | ||
if err != nil || match { | ||
return err | ||
} | ||
|
||
output := c.metaFormatter.Format(path, input) | ||
|
||
if bytes.Equal(input, output) { | ||
return nil | ||
} | ||
|
||
c.log.Infof("format: %s", path) | ||
|
||
// On Windows, we need to re-set the permissions from the file. See golang/go#38225. | ||
var perms os.FileMode | ||
if fi, err := os.Stat(path); err == nil { | ||
perms = fi.Mode() & os.ModePerm | ||
} | ||
|
||
return os.WriteFile(path, output, perms) | ||
}) | ||
} | ||
|
||
type RunnerOptions struct { | ||
basePath string | ||
patterns []*regexp.Regexp | ||
generated string | ||
} | ||
|
||
func NewRunnerOptions(cfg *config.Config) (RunnerOptions, error) { | ||
basePath, err := fsutils.GetBasePath(context.Background(), cfg.Run.RelativePathMode, cfg.GetConfigDir()) | ||
if err != nil { | ||
return RunnerOptions{}, fmt.Errorf("get base path: %w", err) | ||
} | ||
|
||
opts := RunnerOptions{ | ||
basePath: basePath, | ||
generated: cfg.Formatters.Exclusions.Generated, | ||
} | ||
|
||
for _, pattern := range cfg.Formatters.Exclusions.Paths { | ||
exp, err := regexp.Compile(fsutils.NormalizePathInRegex(pattern)) | ||
if err != nil { | ||
return RunnerOptions{}, fmt.Errorf("compile path pattern %q: %w", pattern, err) | ||
} | ||
|
||
opts.patterns = append(opts.patterns, exp) | ||
} | ||
|
||
return opts, nil | ||
} | ||
|
||
func (o RunnerOptions) MatchPatterns(path string) (bool, error) { | ||
ldez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if len(o.patterns) == 0 { | ||
return false, nil | ||
} | ||
|
||
rel, err := filepath.Rel(o.basePath, path) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
for _, pattern := range o.patterns { | ||
if pattern.MatchString(rel) { | ||
return true, nil | ||
} | ||
} | ||
|
||
return false, nil | ||
} | ||
|
||
func skipDir(name string) bool { | ||
switch name { | ||
case "vendor", "testdata", "node_modules": | ||
return true | ||
|
||
case "third_party", "builtin": // For compatibility with `exclude-dirs-use-default`. | ||
return true | ||
bombsimon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
default: | ||
if strings.HasPrefix(name, ".") { | ||
return true | ||
} | ||
|
||
return false | ||
ldez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
func isGoFile(f fs.FileInfo) bool { | ||
return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go") | ||
ldez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.