Skip to content

Commit da211a1

Browse files
committed
feat: new command
1 parent cab91db commit da211a1

File tree

4 files changed

+331
-0
lines changed

4 files changed

+331
-0
lines changed

pkg/commands/flagsets.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ func setupLintersFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
3838
color.GreenString("Override linters configuration section to only run the specific linter(s)")) // Flags only.
3939
}
4040

41+
func setupFormattersFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
42+
internal.AddFlagAndBindP(v, fs, fs.StringSliceP, "enable", "E", "formatters.enable", nil,
43+
color.GreenString("Enable specific formatter"))
44+
}
45+
4146
func setupRunFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
4247
internal.AddFlagAndBindP(v, fs, fs.IntP, "concurrency", "j", "run.concurrency", getDefaultConcurrency(),
4348
color.GreenString("Number of CPUs to use (Default: number of logical CPUs)"))

pkg/commands/fmt.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"log"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/spf13/cobra"
12+
"github.com/spf13/viper"
13+
14+
"github.com/golangci/golangci-lint/pkg/config"
15+
"github.com/golangci/golangci-lint/pkg/goformat"
16+
"github.com/golangci/golangci-lint/pkg/goformatters"
17+
"github.com/golangci/golangci-lint/pkg/logutils"
18+
"github.com/golangci/golangci-lint/pkg/result/processors"
19+
)
20+
21+
type fmtCommand struct {
22+
viper *viper.Viper
23+
cmd *cobra.Command
24+
25+
opts config.LoaderOptions
26+
27+
cfg *config.Config
28+
29+
buildInfo BuildInfo
30+
31+
runner *goformat.Runner
32+
33+
log logutils.Log
34+
debugf logutils.DebugFunc
35+
}
36+
37+
func newFmtCommand(logger logutils.Log, info BuildInfo) *fmtCommand {
38+
c := &fmtCommand{
39+
viper: viper.New(),
40+
log: logger,
41+
debugf: logutils.Debug(logutils.DebugKeyExec),
42+
cfg: config.NewDefault(),
43+
buildInfo: info,
44+
}
45+
46+
fmtCmd := &cobra.Command{
47+
Use: "fmt",
48+
Short: "Format Go source files",
49+
RunE: c.execute,
50+
PreRunE: c.preRunE,
51+
PersistentPreRunE: c.persistentPreRunE,
52+
SilenceUsage: true,
53+
}
54+
55+
fmtCmd.SetOut(logutils.StdOut) // use custom output to properly color it in Windows terminals
56+
fmtCmd.SetErr(logutils.StdErr)
57+
58+
flagSet := fmtCmd.Flags()
59+
flagSet.SortFlags = false // sort them as they are defined here
60+
61+
setupConfigFileFlagSet(flagSet, &c.opts)
62+
63+
setupFormattersFlagSet(c.viper, flagSet)
64+
65+
c.cmd = fmtCmd
66+
67+
return c
68+
}
69+
70+
func (c *fmtCommand) persistentPreRunE(cmd *cobra.Command, args []string) error {
71+
c.log.Infof("%s", c.buildInfo.String())
72+
73+
loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), c.opts, c.cfg, args)
74+
75+
err := loader.Load(config.LoadOptions{CheckDeprecation: true, Validation: true})
76+
if err != nil {
77+
return fmt.Errorf("can't load config: %w", err)
78+
}
79+
80+
return nil
81+
}
82+
83+
func (c *fmtCommand) preRunE(_ *cobra.Command, _ []string) error {
84+
metaFormatter, err := goformatters.NewMetaFormatter(c.log, &c.cfg.Formatters, &c.cfg.Run)
85+
if err != nil {
86+
return fmt.Errorf("failed to create meta-formatter: %w", err)
87+
}
88+
89+
matcher := processors.NewGeneratedFileMatcher(c.cfg.Formatters.Exclusions.Generated)
90+
91+
opts, err := goformat.NewRunnerOptions(c.cfg)
92+
if err != nil {
93+
return fmt.Errorf("build walk options: %w", err)
94+
}
95+
96+
c.runner = goformat.NewRunner(c.log, metaFormatter, matcher, opts)
97+
98+
return nil
99+
}
100+
101+
func (c *fmtCommand) execute(_ *cobra.Command, args []string) error {
102+
if !logutils.HaveDebugTag(logutils.DebugKeyLintersOutput) {
103+
// Don't allow linters and loader to print anything
104+
log.SetOutput(io.Discard)
105+
savedStdout, savedStderr := c.setOutputToDevNull()
106+
defer func() {
107+
os.Stdout, os.Stderr = savedStdout, savedStderr
108+
}()
109+
}
110+
111+
paths, err := cleanArgs(args)
112+
if err != nil {
113+
return fmt.Errorf("failed to clean arguments: %w", err)
114+
}
115+
116+
c.log.Infof("Formatting Go files...")
117+
118+
err = c.runner.Run(paths)
119+
if err != nil {
120+
return fmt.Errorf("failed to process files: %w", err)
121+
}
122+
123+
return nil
124+
}
125+
126+
func (c *fmtCommand) setOutputToDevNull() (savedStdout, savedStderr *os.File) {
127+
savedStdout, savedStderr = os.Stdout, os.Stderr
128+
devNull, err := os.Open(os.DevNull)
129+
if err != nil {
130+
c.log.Warnf("Can't open null device %q: %s", os.DevNull, err)
131+
return
132+
}
133+
134+
os.Stdout, os.Stderr = devNull, devNull
135+
return
136+
}
137+
138+
func cleanArgs(args []string) ([]string, error) {
139+
if len(args) == 0 {
140+
abs, err := filepath.Abs(".")
141+
if err != nil {
142+
return nil, err
143+
}
144+
145+
return []string{abs}, nil
146+
}
147+
148+
var expended []string
149+
for _, arg := range args {
150+
abs, err := filepath.Abs(strings.ReplaceAll(arg, "...", ""))
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
expended = append(expended, abs)
156+
}
157+
158+
return expended, nil
159+
}

pkg/commands/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func newRootCommand(info BuildInfo) *rootCommand {
6060
rootCmd.AddCommand(
6161
newLintersCommand(log).cmd,
6262
newRunCommand(log, info).cmd,
63+
newFmtCommand(log, info).cmd,
6364
newCacheCommand().cmd,
6465
newConfigCommand(log, info).cmd,
6566
newVersionCommand(info).cmd,

pkg/goformat/runner.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package goformat
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io/fs"
8+
"os"
9+
"path/filepath"
10+
"regexp"
11+
"strings"
12+
13+
"github.com/golangci/golangci-lint/pkg/config"
14+
"github.com/golangci/golangci-lint/pkg/fsutils"
15+
"github.com/golangci/golangci-lint/pkg/goformatters"
16+
"github.com/golangci/golangci-lint/pkg/logutils"
17+
"github.com/golangci/golangci-lint/pkg/result/processors"
18+
)
19+
20+
type Runner struct {
21+
log logutils.Log
22+
23+
metaFormatter *goformatters.MetaFormatter
24+
matcher *processors.GeneratedFileMatcher
25+
26+
opts RunnerOptions
27+
}
28+
29+
func NewRunner(log logutils.Log,
30+
metaFormatter *goformatters.MetaFormatter, matcher *processors.GeneratedFileMatcher,
31+
opts RunnerOptions) *Runner {
32+
return &Runner{
33+
log: log,
34+
matcher: matcher,
35+
metaFormatter: metaFormatter,
36+
opts: opts,
37+
}
38+
}
39+
40+
func (c *Runner) Run(paths []string) error {
41+
for _, path := range paths {
42+
err := c.walk(path)
43+
if err != nil {
44+
return err
45+
}
46+
}
47+
48+
return nil
49+
}
50+
51+
func (c *Runner) walk(root string) error {
52+
return filepath.Walk(root, func(path string, f fs.FileInfo, err error) error {
53+
if err != nil {
54+
return err
55+
}
56+
57+
if f.IsDir() && skipDir(f.Name()) {
58+
return fs.SkipDir
59+
}
60+
61+
// Ignore non-Go files.
62+
if !isGoFile(f) {
63+
return nil
64+
}
65+
66+
match, err := c.opts.MatchPatterns(path)
67+
if err != nil || match {
68+
return err
69+
}
70+
71+
input, err := os.ReadFile(path)
72+
if err != nil {
73+
return err
74+
}
75+
76+
match, err = c.matcher.IsGeneratedFile(path, input)
77+
if err != nil || match {
78+
return err
79+
}
80+
81+
output := c.metaFormatter.Format(path, input)
82+
83+
if bytes.Equal(input, output) {
84+
return nil
85+
}
86+
87+
c.log.Infof("format: %s", path)
88+
89+
// On Windows, we need to re-set the permissions from the file. See golang/go#38225.
90+
var perms os.FileMode
91+
if fi, err := os.Stat(path); err == nil {
92+
perms = fi.Mode() & os.ModePerm
93+
}
94+
95+
return os.WriteFile(path, output, perms)
96+
})
97+
}
98+
99+
type RunnerOptions struct {
100+
basePath string
101+
patterns []*regexp.Regexp
102+
generated string
103+
}
104+
105+
func NewRunnerOptions(cfg *config.Config) (RunnerOptions, error) {
106+
basePath, err := fsutils.GetBasePath(context.Background(), cfg.Run.RelativePathMode, cfg.GetConfigDir())
107+
if err != nil {
108+
return RunnerOptions{}, fmt.Errorf("get base path: %w", err)
109+
}
110+
111+
opts := RunnerOptions{
112+
basePath: basePath,
113+
generated: cfg.Formatters.Exclusions.Generated,
114+
}
115+
116+
for _, pattern := range cfg.Formatters.Exclusions.Paths {
117+
exp, err := regexp.Compile(fsutils.NormalizePathInRegex(pattern))
118+
if err != nil {
119+
return RunnerOptions{}, fmt.Errorf("compile path pattern %q: %w", pattern, err)
120+
}
121+
122+
opts.patterns = append(opts.patterns, exp)
123+
}
124+
125+
return opts, nil
126+
}
127+
128+
func (o RunnerOptions) MatchPatterns(path string) (bool, error) {
129+
if len(o.patterns) == 0 {
130+
return false, nil
131+
}
132+
133+
rel, err := filepath.Rel(o.basePath, path)
134+
if err != nil {
135+
return false, err
136+
}
137+
138+
for _, pattern := range o.patterns {
139+
if pattern.MatchString(rel) {
140+
return true, nil
141+
}
142+
}
143+
144+
return false, nil
145+
}
146+
147+
func skipDir(name string) bool {
148+
switch name {
149+
case "vendor", "testdata", "node_modules":
150+
return true
151+
152+
case "third_party", "builtin": // For compatibility with `exclude-dirs-use-default`.
153+
return true
154+
155+
default:
156+
if strings.HasPrefix(name, ".") {
157+
return true
158+
}
159+
160+
return false
161+
}
162+
}
163+
164+
func isGoFile(f fs.FileInfo) bool {
165+
return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go")
166+
}

0 commit comments

Comments
 (0)