diff --git a/go.mod b/go.mod index 8d51b3bd474f..dab43000b14f 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/gofrs/flock v0.12.1 github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a github.com/golangci/go-printf-func-name v0.1.0 - github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 + github.com/golangci/gofmt v0.0.0-20241223200906-057b0627d9b9 github.com/golangci/misspell v0.6.0 github.com/golangci/plugin-module-register v0.1.1 github.com/golangci/revgrep v0.5.3 diff --git a/go.sum b/go.sum index 86beb653f59e..a653aea44362 100644 --- a/go.sum +++ b/go.sum @@ -234,8 +234,8 @@ github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9 github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= -github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 h1:/1322Qns6BtQxUZDTAT4SdcoxknUki7IAoK4SAXr8ME= -github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9/go.mod h1:Oesb/0uFAyWoaw1U1qS5zyjCg5NP9C9iwjnI4tIsXEE= +github.com/golangci/gofmt v0.0.0-20241223200906-057b0627d9b9 h1:t5wybL6RtO83VwoMOb7U/Peqe3gGKQlPIC66wXmnkvM= +github.com/golangci/gofmt v0.0.0-20241223200906-057b0627d9b9/go.mod h1:Ag3L7sh7E28qAp/5xnpMMTuGYqxLZoSaEHZDkZB1RgU= github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= github.com/golangci/modinfo v0.3.3 h1:YBQDZpDMJpe5mtd0klUFYL8tSVkmF3cmm0fZ48sc7+s= diff --git a/pkg/config/config.go b/pkg/config/config.go index 11b55955d957..579eddf594a2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -93,32 +93,15 @@ func detectGoVersion() string { // else it returns `go` version if present, // else it returns empty. func detectGoVersionFromGoMod() string { - info, err := gomod.GetModuleInfo() - if err != nil { - return "" - } - - wd, err := os.Getwd() + modPath, err := gomod.GetGoModPath() if err != nil { - return "" - } - - slices.SortFunc(info, func(a, b gomod.ModInfo) int { - return cmp.Compare(len(b.Path), len(a.Path)) - }) - - goMod := info[0] - for _, m := range info { - if !strings.HasPrefix(wd, m.Dir) { - continue + modPath = detectGoModFallback() + if modPath == "" { + return "" } - - goMod = m - - break } - file, err := parseGoMod(goMod.GoMod) + file, err := parseGoMod(modPath) if err != nil { return "" } @@ -144,3 +127,32 @@ func parseGoMod(goMod string) (*modfile.File, error) { return modfile.Parse("go.mod", raw, nil) } + +func detectGoModFallback() string { + info, err := gomod.GetModuleInfo() + if err != nil { + return "" + } + + wd, err := os.Getwd() + if err != nil { + return "" + } + + slices.SortFunc(info, func(a, b gomod.ModInfo) int { + return cmp.Compare(len(b.Path), len(a.Path)) + }) + + goMod := info[0] + for _, m := range info { + if !strings.HasPrefix(wd, m.Dir) { + continue + } + + goMod = m + + break + } + + return goMod.GoMod +} diff --git a/pkg/goformatters/formatters.go b/pkg/goformatters/formatters.go new file mode 100644 index 000000000000..c8953ad3bf02 --- /dev/null +++ b/pkg/goformatters/formatters.go @@ -0,0 +1,6 @@ +package goformatters + +type Formatter interface { + Name() string + Format(filename string, src []byte) ([]byte, error) +} diff --git a/pkg/goformatters/gci/gci.go b/pkg/goformatters/gci/gci.go new file mode 100644 index 000000000000..bae39f3cee6d --- /dev/null +++ b/pkg/goformatters/gci/gci.go @@ -0,0 +1,48 @@ +package gci + +import ( + gcicfg "github.com/daixiang0/gci/pkg/config" + "github.com/daixiang0/gci/pkg/gci" + "github.com/ldez/grignotin/gomod" + + "github.com/golangci/golangci-lint/pkg/config" +) + +const Name = "gci" + +type Formatter struct { + config *gcicfg.Config +} + +func New(cfg config.GciSettings) (*Formatter, error) { + modPath, err := gomod.GetModulePath() + if err != nil { + return nil, err + } + + parsedCfg, err := gcicfg.YamlConfig{ + Cfg: gcicfg.BoolConfig{ + NoInlineComments: cfg.NoInlineComments, + NoPrefixComments: cfg.NoPrefixComments, + SkipGenerated: cfg.SkipGenerated, + CustomOrder: cfg.CustomOrder, + NoLexOrder: cfg.NoLexOrder, + }, + SectionStrings: cfg.Sections, + ModPath: modPath, + }.Parse() + if err != nil { + return nil, err + } + + return &Formatter{config: parsedCfg}, nil +} + +func (*Formatter) Name() string { + return Name +} + +func (f *Formatter) Format(filename string, src []byte) ([]byte, error) { + _, formatted, err := gci.LoadFormat(src, filename, *f.config) + return formatted, err +} diff --git a/pkg/goformatters/gofmt/gofmt.go b/pkg/goformatters/gofmt/gofmt.go new file mode 100644 index 000000000000..fe2e355590ed --- /dev/null +++ b/pkg/goformatters/gofmt/gofmt.go @@ -0,0 +1,35 @@ +package gofmt + +import ( + "github.com/golangci/gofmt/gofmt" + + "github.com/golangci/golangci-lint/pkg/config" +) + +const Name = "gofmt" + +type Formatter struct { + options gofmt.Options +} + +func New(cfg config.GoFmtSettings) *Formatter { + var rewriteRules []gofmt.RewriteRule + for _, rule := range cfg.RewriteRules { + rewriteRules = append(rewriteRules, gofmt.RewriteRule(rule)) + } + + return &Formatter{ + options: gofmt.Options{ + NeedSimplify: cfg.Simplify, + RewriteRules: rewriteRules, + }, + } +} + +func (*Formatter) Name() string { + return Name +} + +func (f *Formatter) Format(filename string, src []byte) ([]byte, error) { + return gofmt.Source(filename, src, f.options) +} diff --git a/pkg/goformatters/gofumpt/gofumpt.go b/pkg/goformatters/gofumpt/gofumpt.go new file mode 100644 index 000000000000..f4605e2333d7 --- /dev/null +++ b/pkg/goformatters/gofumpt/gofumpt.go @@ -0,0 +1,43 @@ +package gofumpt + +import ( + "strings" + + gofumpt "mvdan.cc/gofumpt/format" + + "github.com/golangci/golangci-lint/pkg/config" +) + +const Name = "gofumpt" + +type Formatter struct { + options gofumpt.Options +} + +func New(cfg config.GofumptSettings, goVersion string) *Formatter { + return &Formatter{ + options: gofumpt.Options{ + LangVersion: getLangVersion(goVersion), + ModulePath: cfg.ModulePath, + ExtraRules: cfg.ExtraRules, + }, + } +} + +func (*Formatter) Name() string { + return Name +} + +func (f *Formatter) Format(_ string, src []byte) ([]byte, error) { + return gofumpt.Source(src, f.options) +} + +// modified copy of pkg/golinters/gofumpt/gofumpt.go +func getLangVersion(v string) string { + if v == "" { + // TODO: defaults to "1.15", in the future (v2) must be removed. + return "go1.15" + } + + return "go" + strings.TrimPrefix(v, "go") +} diff --git a/pkg/goformatters/goimports/goimports.go b/pkg/goformatters/goimports/goimports.go new file mode 100644 index 000000000000..add9eb3a61a2 --- /dev/null +++ b/pkg/goformatters/goimports/goimports.go @@ -0,0 +1,22 @@ +package goimports + +import ( + "golang.org/x/tools/imports" +) + +const Name = "goimports" + +type Formatter struct{} + +func New() *Formatter { + return &Formatter{} +} + +func (*Formatter) Name() string { + return Name +} + +func (*Formatter) Format(filename string, src []byte) ([]byte, error) { + // The `imports.LocalPrefix` (`settings.LocalPrefixes`) is a global var. + return imports.Process(filename, src, nil) +} diff --git a/pkg/lint/runner.go b/pkg/lint/runner.go index 2c47c7166e6a..5a40c58ea6d4 100644 --- a/pkg/lint/runner.go +++ b/pkg/lint/runner.go @@ -60,6 +60,11 @@ func NewRunner(log logutils.Log, cfg *config.Config, args []string, goenv *gouti return nil, fmt.Errorf("failed to get enabled linters: %w", err) } + formatter, err := processors.NewFormatter(log, cfg, enabledLinters) + if err != nil { + return nil, fmt.Errorf("failed to create formatter: %w", err) + } + return &Runner{ Processors: []processors.Processor{ processors.NewCgo(goenv), @@ -95,6 +100,8 @@ func NewRunner(log logutils.Log, cfg *config.Config, args []string, goenv *gouti // The fixer still needs to see paths for the issues that are relative to the current directory. processors.NewFixer(cfg, log, fileCache), + // The formatter needs to be after the fixer and the last processor that write files. + formatter, // Now we can modify the issues for output. processors.NewPathPrefixer(cfg.Output.PathPrefix), diff --git a/pkg/result/processors/fixer.go b/pkg/result/processors/fixer.go index 67b603a09bc0..f6a40c92bbfb 100644 --- a/pkg/result/processors/fixer.go +++ b/pkg/result/processors/fixer.go @@ -18,6 +18,10 @@ import ( "github.com/golangci/golangci-lint/internal/x/tools/diff" "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/fsutils" + "github.com/golangci/golangci-lint/pkg/goformatters/gci" + "github.com/golangci/golangci-lint/pkg/goformatters/gofmt" + "github.com/golangci/golangci-lint/pkg/goformatters/gofumpt" + "github.com/golangci/golangci-lint/pkg/goformatters/goimports" "github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/timeutils" @@ -71,11 +75,18 @@ func (p Fixer) process(issues []result.Issue) ([]result.Issue, error) { // filenames / linters / edits editsByLinter := make(map[string]map[string][]diff.Edit) + formatters := []string{gofumpt.Name, goimports.Name, gofmt.Name, gci.Name} + var notFixableIssues []result.Issue for i := range issues { issue := issues[i] + if slices.Contains(formatters, issue.FromLinter) { + notFixableIssues = append(notFixableIssues, issue) + continue + } + if issue.SuggestedFixes == nil || skipNoTextEdit(&issue) { notFixableIssues = append(notFixableIssues, issue) continue diff --git a/pkg/result/processors/formatter.go b/pkg/result/processors/formatter.go new file mode 100644 index 000000000000..4a31c5bdbba5 --- /dev/null +++ b/pkg/result/processors/formatter.go @@ -0,0 +1,128 @@ +package processors + +import ( + "bytes" + "fmt" + "os" + "slices" + + "github.com/golangci/golangci-lint/pkg/config" + "github.com/golangci/golangci-lint/pkg/goformatters" + "github.com/golangci/golangci-lint/pkg/goformatters/gci" + "github.com/golangci/golangci-lint/pkg/goformatters/gofmt" + "github.com/golangci/golangci-lint/pkg/goformatters/gofumpt" + "github.com/golangci/golangci-lint/pkg/goformatters/goimports" + "github.com/golangci/golangci-lint/pkg/lint/linter" + "github.com/golangci/golangci-lint/pkg/logutils" + "github.com/golangci/golangci-lint/pkg/result" +) + +// Formatter runs all the "formatters". +// It should be run after the [Fixer] because: +// - The code format is applied after the fixes to avoid changing positions. +// - The [Fixer] writes the files on the disk (so the file cache cannot be used as it contains the files before fixes). +type Formatter struct { + log logutils.Log + cfg *config.Config + formatters []goformatters.Formatter +} + +// NewFormatter creates a new [Formatter]. +func NewFormatter(log logutils.Log, cfg *config.Config, enabledLinters map[string]*linter.Config) (*Formatter, error) { + p := &Formatter{ + log: log, + cfg: cfg, + } + + if _, ok := enabledLinters[gofmt.Name]; ok { + p.formatters = append(p.formatters, gofmt.New(cfg.LintersSettings.Gofmt)) + } + + if _, ok := enabledLinters[gofumpt.Name]; ok { + p.formatters = append(p.formatters, gofumpt.New(cfg.LintersSettings.Gofumpt, cfg.Run.Go)) + } + + if _, ok := enabledLinters[goimports.Name]; ok { + p.formatters = append(p.formatters, goimports.New()) + } + + // gci is a last because the only goal of gci is to handle imports. + if _, ok := enabledLinters[gci.Name]; ok { + formatter, err := gci.New(cfg.LintersSettings.Gci) + if err != nil { + return nil, fmt.Errorf("gci: creating formatter: %w", err) + } + + p.formatters = append(p.formatters, formatter) + } + + return p, nil +} + +func (*Formatter) Name() string { + return "formatter" +} + +func (p *Formatter) Process(issues []result.Issue) ([]result.Issue, error) { + if !p.cfg.Issues.NeedFix { + return issues, nil + } + + if len(p.formatters) == 0 { + return issues, nil + } + + all := []string{gofumpt.Name, goimports.Name, gofmt.Name, gci.Name} + + var notFixableIssues []result.Issue + + files := make(map[string]struct{}) + + for i := range issues { + issue := issues[i] + + if slices.Contains(all, issue.FromLinter) { + files[issue.FilePath()] = struct{}{} + } else { + notFixableIssues = append(notFixableIssues, issue) + } + } + + for target := range files { + content, err := os.ReadFile(target) + if err != nil { + p.log.Warnf("Error reading file %s: %v", target, err) + continue + } + + formatted := p.format(target, content) + if bytes.Equal(content, formatted) { + continue + } + + err = os.WriteFile(target, formatted, filePerm) + if err != nil { + p.log.Warnf("Writing file %s: %v", target, err) + } + } + + return notFixableIssues, nil +} + +func (p *Formatter) format(filename string, src []byte) []byte { + data := bytes.Clone(src) + + for _, formatter := range p.formatters { + formatted, err := formatter.Format(filename, data) + if err != nil { + p.log.Warnf("(%s) formatting file %s: %v", formatter.Name(), filename, err) + continue + } + + data = formatted + } + + return data +} + +func (*Formatter) Finish() {}