Skip to content

Commit 784264d

Browse files
authoredFeb 26, 2024
dev: new commands system (#4412)
·
v2.6.1v1.57.0
1 parent b5d7302 commit 784264d

File tree

20 files changed

+1180
-1144
lines changed

20 files changed

+1180
-1144
lines changed
 

‎cmd/golangci-lint/main.go‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ func main() {
3636
Date: date,
3737
}
3838

39-
e := commands.NewExecutor(info)
40-
41-
if err := e.Execute(); err != nil {
42-
fmt.Fprintf(os.Stderr, "failed executing command with error %v\n", err)
39+
if err := commands.Execute(info); err != nil {
40+
_, _ = fmt.Fprintf(os.Stderr, "failed executing command with error %v\n", err)
4341
os.Exit(exitcodes.Failure)
4442
}
4543
}

‎docs/src/docs/contributing/architecture.mdx‎

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -22,90 +22,6 @@ graph LR
2222

2323
</ResponsiveContainer>
2424

25-
## Init
26-
27-
The execution starts here:
28-
29-
```go title=cmd/golangci-lint/main.go
30-
func main() {
31-
e := commands.NewExecutor(info)
32-
33-
if err := e.Execute(); err != nil {
34-
fmt.Fprintf(os.Stderr, "failed executing command with error %v\n", err)
35-
os.Exit(exitcodes.Failure)
36-
}
37-
}
38-
```
39-
40-
The **executer** is our abstraction:
41-
42-
```go title=pkg/commands/executor.go
43-
type Executor struct {
44-
rootCmd *cobra.Command
45-
runCmd *cobra.Command
46-
lintersCmd *cobra.Command
47-
48-
exitCode int
49-
buildInfo BuildInfo
50-
51-
cfg *config.Config
52-
log logutils.Log
53-
reportData report.Data
54-
DBManager *lintersdb.Manager
55-
EnabledLintersSet *lintersdb.EnabledSet
56-
contextLoader *lint.ContextLoader
57-
goenv *goutil.Env
58-
fileCache *fsutils.FileCache
59-
lineCache *fsutils.LineCache
60-
pkgCache *pkgcache.Cache
61-
debugf logutils.DebugFunc
62-
sw *timeutils.Stopwatch
63-
64-
loadGuard *load.Guard
65-
flock *flock.Flock
66-
}
67-
```
68-
69-
We use dependency injection and all root dependencies are stored in this executor.
70-
71-
In the function `NewExecutor` we do the following:
72-
73-
1. Initialize dependencies.
74-
2. Initialize [cobra](https://github.com/spf13/cobra) commands.
75-
3. Parse the config file using [viper](https://github.com/spf13/viper) and merge it with command line arguments.
76-
77-
The following execution is controlled by `cobra`. If a user executes `golangci-lint run`
78-
then `cobra` executes `e.runCmd`.
79-
80-
Different `cobra` commands have different runners, e.g. a `run` command is configured in the following way:
81-
82-
```go title=pkg/commands/run.go
83-
func (e *Executor) initRun() {
84-
e.runCmd = &cobra.Command{
85-
Use: "run",
86-
Short: "Run the linters",
87-
Run: e.executeRun,
88-
PreRunE: func(_ *cobra.Command, _ []string) error {
89-
if ok := e.acquireFileLock(); !ok {
90-
return errors.New("parallel golangci-lint is running")
91-
}
92-
return nil
93-
},
94-
PostRun: func(_ *cobra.Command, _ []string) {
95-
e.releaseFileLock()
96-
},
97-
}
98-
e.rootCmd.AddCommand(e.runCmd)
99-
100-
e.runCmd.SetOut(logutils.StdOut) // use custom output to properly color it in Windows terminals
101-
e.runCmd.SetErr(logutils.StdErr)
102-
103-
e.initRunConfiguration(e.runCmd)
104-
}
105-
```
106-
107-
The primary execution function of the `run` command is `executeRun`.
108-
10925
## Load Packages
11026

11127
Loading packages is listing all packages and their recursive dependencies for analysis.

‎pkg/commands/cache.go‎

Lines changed: 30 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
package commands
22

33
import (
4-
"bytes"
5-
"crypto/sha256"
64
"fmt"
7-
"io"
85
"os"
96
"path/filepath"
10-
"strings"
117

128
"github.com/spf13/cobra"
13-
"gopkg.in/yaml.v3"
149

1510
"github.com/golangci/golangci-lint/internal/cache"
16-
"github.com/golangci/golangci-lint/pkg/config"
1711
"github.com/golangci/golangci-lint/pkg/fsutils"
1812
"github.com/golangci/golangci-lint/pkg/logutils"
1913
)
2014

21-
func (e *Executor) initCache() {
15+
type cacheCommand struct {
16+
cmd *cobra.Command
17+
}
18+
19+
func newCacheCommand() *cacheCommand {
20+
c := &cacheCommand{}
21+
2222
cacheCmd := &cobra.Command{
2323
Use: "cache",
2424
Short: "Cache control and information",
@@ -28,42 +28,45 @@ func (e *Executor) initCache() {
2828
},
2929
}
3030

31-
cacheCmd.AddCommand(&cobra.Command{
32-
Use: "clean",
33-
Short: "Clean cache",
34-
Args: cobra.NoArgs,
35-
ValidArgsFunction: cobra.NoFileCompletions,
36-
RunE: e.executeCacheClean,
37-
})
38-
cacheCmd.AddCommand(&cobra.Command{
39-
Use: "status",
40-
Short: "Show cache status",
41-
Args: cobra.NoArgs,
42-
ValidArgsFunction: cobra.NoFileCompletions,
43-
Run: e.executeCacheStatus,
44-
})
31+
cacheCmd.AddCommand(
32+
&cobra.Command{
33+
Use: "clean",
34+
Short: "Clean cache",
35+
Args: cobra.NoArgs,
36+
ValidArgsFunction: cobra.NoFileCompletions,
37+
RunE: c.executeClean,
38+
},
39+
&cobra.Command{
40+
Use: "status",
41+
Short: "Show cache status",
42+
Args: cobra.NoArgs,
43+
ValidArgsFunction: cobra.NoFileCompletions,
44+
Run: c.executeStatus,
45+
},
46+
)
4547

46-
// TODO: add trim command?
48+
c.cmd = cacheCmd
4749

48-
e.rootCmd.AddCommand(cacheCmd)
50+
return c
4951
}
5052

51-
func (e *Executor) executeCacheClean(_ *cobra.Command, _ []string) error {
53+
func (c *cacheCommand) executeClean(_ *cobra.Command, _ []string) error {
5254
cacheDir := cache.DefaultDir()
55+
5356
if err := os.RemoveAll(cacheDir); err != nil {
5457
return fmt.Errorf("failed to remove dir %s: %w", cacheDir, err)
5558
}
5659

5760
return nil
5861
}
5962

60-
func (e *Executor) executeCacheStatus(_ *cobra.Command, _ []string) {
63+
func (c *cacheCommand) executeStatus(_ *cobra.Command, _ []string) {
6164
cacheDir := cache.DefaultDir()
62-
fmt.Fprintf(logutils.StdOut, "Dir: %s\n", cacheDir)
65+
_, _ = fmt.Fprintf(logutils.StdOut, "Dir: %s\n", cacheDir)
6366

6467
cacheSizeBytes, err := dirSizeBytes(cacheDir)
6568
if err == nil {
66-
fmt.Fprintf(logutils.StdOut, "Size: %s\n", fsutils.PrettifyBytesCount(cacheSizeBytes))
69+
_, _ = fmt.Fprintf(logutils.StdOut, "Size: %s\n", fsutils.PrettifyBytesCount(cacheSizeBytes))
6770
}
6871
}
6972

@@ -77,68 +80,3 @@ func dirSizeBytes(path string) (int64, error) {
7780
})
7881
return size, err
7982
}
80-
81-
// --- Related to cache but not used directly by the cache command.
82-
83-
func initHashSalt(version string, cfg *config.Config) error {
84-
binSalt, err := computeBinarySalt(version)
85-
if err != nil {
86-
return fmt.Errorf("failed to calculate binary salt: %w", err)
87-
}
88-
89-
configSalt, err := computeConfigSalt(cfg)
90-
if err != nil {
91-
return fmt.Errorf("failed to calculate config salt: %w", err)
92-
}
93-
94-
b := bytes.NewBuffer(binSalt)
95-
b.Write(configSalt)
96-
cache.SetSalt(b.Bytes())
97-
return nil
98-
}
99-
100-
func computeBinarySalt(version string) ([]byte, error) {
101-
if version != "" && version != "(devel)" {
102-
return []byte(version), nil
103-
}
104-
105-
if logutils.HaveDebugTag(logutils.DebugKeyBinSalt) {
106-
return []byte("debug"), nil
107-
}
108-
109-
p, err := os.Executable()
110-
if err != nil {
111-
return nil, err
112-
}
113-
f, err := os.Open(p)
114-
if err != nil {
115-
return nil, err
116-
}
117-
defer f.Close()
118-
h := sha256.New()
119-
if _, err := io.Copy(h, f); err != nil {
120-
return nil, err
121-
}
122-
return h.Sum(nil), nil
123-
}
124-
125-
// computeConfigSalt computes configuration hash.
126-
// We don't hash all config fields to reduce meaningless cache invalidations.
127-
// At least, it has a huge impact on tests speed.
128-
// Fields: `LintersSettings` and `Run.BuildTags`.
129-
func computeConfigSalt(cfg *config.Config) ([]byte, error) {
130-
lintersSettingsBytes, err := yaml.Marshal(cfg.LintersSettings)
131-
if err != nil {
132-
return nil, fmt.Errorf("failed to json marshal config linter settings: %w", err)
133-
}
134-
135-
configData := bytes.NewBufferString("linters-settings=")
136-
configData.Write(lintersSettingsBytes)
137-
configData.WriteString("\nbuild-tags=%s" + strings.Join(cfg.Run.BuildTags, ","))
138-
139-
h := sha256.New()
140-
if _, err := h.Write(configData.Bytes()); err != nil {
141-
return nil, err
142-
}
143-
return h.Sum(nil), nil
144-
}

‎pkg/commands/config.go‎

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,27 @@ import (
55
"os"
66

77
"github.com/spf13/cobra"
8-
"github.com/spf13/pflag"
98
"github.com/spf13/viper"
109

1110
"github.com/golangci/golangci-lint/pkg/config"
1211
"github.com/golangci/golangci-lint/pkg/exitcodes"
1312
"github.com/golangci/golangci-lint/pkg/fsutils"
13+
"github.com/golangci/golangci-lint/pkg/logutils"
1414
)
1515

16-
func (e *Executor) initConfig() {
16+
type configCommand struct {
17+
viper *viper.Viper
18+
cmd *cobra.Command
19+
20+
log logutils.Log
21+
}
22+
23+
func newConfigCommand(log logutils.Log) *configCommand {
24+
c := &configCommand{
25+
viper: viper.New(),
26+
log: log,
27+
}
28+
1729
configCmd := &cobra.Command{
1830
Use: "config",
1931
Short: "Config file information",
@@ -23,25 +35,38 @@ func (e *Executor) initConfig() {
2335
},
2436
}
2537

26-
pathCmd := &cobra.Command{
27-
Use: "path",
28-
Short: "Print used config path",
29-
Args: cobra.NoArgs,
30-
ValidArgsFunction: cobra.NoFileCompletions,
31-
Run: e.executePath,
32-
}
38+
configCmd.AddCommand(
39+
&cobra.Command{
40+
Use: "path",
41+
Short: "Print used config path",
42+
Args: cobra.NoArgs,
43+
ValidArgsFunction: cobra.NoFileCompletions,
44+
Run: c.execute,
45+
PreRunE: c.preRunE,
46+
},
47+
)
3348

34-
fs := pathCmd.Flags()
35-
fs.SortFlags = false // sort them as they are defined here
49+
c.cmd = configCmd
3650

37-
configCmd.AddCommand(pathCmd)
38-
e.rootCmd.AddCommand(configCmd)
51+
return c
3952
}
4053

41-
func (e *Executor) executePath(_ *cobra.Command, _ []string) {
42-
usedConfigFile := e.getUsedConfig()
54+
func (c *configCommand) preRunE(cmd *cobra.Command, _ []string) error {
55+
// The command doesn't depend on the real configuration.
56+
// It only needs to know the path of the configuration file.
57+
loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), config.LoaderOptions{}, config.NewDefault())
58+
59+
if err := loader.Load(); err != nil {
60+
return fmt.Errorf("can't load config: %w", err)
61+
}
62+
63+
return nil
64+
}
65+
66+
func (c *configCommand) execute(_ *cobra.Command, _ []string) {
67+
usedConfigFile := c.getUsedConfig()
4368
if usedConfigFile == "" {
44-
e.log.Warnf("No config file detected")
69+
c.log.Warnf("No config file detected")
4570
os.Exit(exitcodes.NoConfigFileDetected)
4671
}
4772

@@ -50,24 +75,17 @@ func (e *Executor) executePath(_ *cobra.Command, _ []string) {
5075

5176
// getUsedConfig returns the resolved path to the golangci config file,
5277
// or the empty string if no configuration could be found.
53-
func (e *Executor) getUsedConfig() string {
54-
usedConfigFile := viper.ConfigFileUsed()
78+
func (c *configCommand) getUsedConfig() string {
79+
usedConfigFile := c.viper.ConfigFileUsed()
5580
if usedConfigFile == "" {
5681
return ""
5782
}
5883

5984
prettyUsedConfigFile, err := fsutils.ShortestRelPath(usedConfigFile, "")
6085
if err != nil {
61-
e.log.Warnf("Can't pretty print config file path: %s", err)
86+
c.log.Warnf("Can't pretty print config file path: %s", err)
6287
return usedConfigFile
6388
}
6489

6590
return prettyUsedConfigFile
6691
}
67-
68-
// --- Related to config but not used directly by the config command.
69-
70-
func initConfigFileFlagSet(fs *pflag.FlagSet, cfg *config.Run) {
71-
fs.StringVarP(&cfg.Config, "config", "c", "", wh("Read config from file path `PATH`"))
72-
fs.BoolVar(&cfg.NoConfig, "no-config", false, wh("Don't read config file"))
73-
}

‎pkg/commands/executor.go‎

Lines changed: 0 additions & 230 deletions
This file was deleted.

‎pkg/commands/flagsets.go‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/fatih/color"
8+
"github.com/spf13/pflag"
9+
"github.com/spf13/viper"
10+
11+
"github.com/golangci/golangci-lint/pkg/commands/internal"
12+
"github.com/golangci/golangci-lint/pkg/config"
13+
"github.com/golangci/golangci-lint/pkg/exitcodes"
14+
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
15+
)
16+
17+
func setupLintersFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
18+
fs.StringSliceP("disable", "D", nil, color.GreenString("Disable specific linter")) // Hack see Loader.applyStringSliceHack
19+
internal.AddFlagAndBind(v, fs, fs.Bool, "disable-all", "linters.disable-all", false, color.GreenString("Disable all linters"))
20+
21+
fs.StringSliceP("enable", "E", nil, color.GreenString("Enable specific linter")) // Hack see Loader.applyStringSliceHack
22+
internal.AddFlagAndBind(v, fs, fs.Bool, "enable-all", "linters.enable-all", false, color.GreenString("Enable all linters"))
23+
24+
internal.AddFlagAndBind(v, fs, fs.Bool, "fast", "linters.fast", false,
25+
color.GreenString("Enable only fast linters from enabled linters set (first run won't be fast)"))
26+
27+
// Hack see Loader.applyStringSliceHack
28+
fs.StringSliceP("presets", "p", nil,
29+
color.GreenString(fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint help linters' to see "+
30+
"them. This option implies option --disable-all", strings.Join(lintersdb.AllPresets(), "|"))))
31+
}
32+
33+
func setupRunFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
34+
internal.AddFlagAndBindP(v, fs, fs.IntP, "concurrency", "j", "run.concurrency", getDefaultConcurrency(),
35+
color.GreenString("Number of CPUs to use (Default: number of logical CPUs)"))
36+
37+
internal.AddFlagAndBind(v, fs, fs.String, "modules-download-mode", "run.modules-download-mode", "",
38+
color.GreenString("Modules download mode. If not empty, passed as -mod=<mode> to go tools"))
39+
internal.AddFlagAndBind(v, fs, fs.Int, "issues-exit-code", "run.issues-exit-code", exitcodes.IssuesFound,
40+
color.GreenString("Exit code when issues were found"))
41+
internal.AddFlagAndBind(v, fs, fs.String, "go", "run.go", "", color.GreenString("Targeted Go version"))
42+
fs.StringSlice("build-tags", nil, color.GreenString("Build tags")) // Hack see Loader.applyStringSliceHack
43+
44+
internal.AddFlagAndBind(v, fs, fs.Duration, "timeout", "run.timeout", defaultTimeout, color.GreenString("Timeout for total work"))
45+
46+
internal.AddFlagAndBind(v, fs, fs.Bool, "tests", "run.tests", true, color.GreenString("Analyze tests (*_test.go)"))
47+
fs.StringSlice("skip-dirs", nil, color.GreenString("Regexps of directories to skip")) // Hack see Loader.applyStringSliceHack
48+
internal.AddFlagAndBind(v, fs, fs.Bool, "skip-dirs-use-default", "run.skip-dirs-use-default", true, getDefaultDirectoryExcludeHelp())
49+
fs.StringSlice("skip-files", nil, color.GreenString("Regexps of files to skip")) // Hack see Loader.applyStringSliceHack
50+
51+
const allowParallelDesc = "Allow multiple parallel golangci-lint instances running. " +
52+
"If false (default) - golangci-lint acquires file lock on start."
53+
internal.AddFlagAndBind(v, fs, fs.Bool, "allow-parallel-runners", "run.allow-parallel-runners", false,
54+
color.GreenString(allowParallelDesc))
55+
const allowSerialDesc = "Allow multiple golangci-lint instances running, but serialize them around a lock. " +
56+
"If false (default) - golangci-lint exits with an error if it fails to acquire file lock on start."
57+
internal.AddFlagAndBind(v, fs, fs.Bool, "allow-serial-runners", "run.allow-serial-runners", false, color.GreenString(allowSerialDesc))
58+
internal.AddFlagAndBind(v, fs, fs.Bool, "show-stats", "run.show-stats", false, color.GreenString("Show statistics per linter"))
59+
}
60+
61+
func setupOutputFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
62+
internal.AddFlagAndBind(v, fs, fs.String, "out-format", "output.format", config.OutFormatColoredLineNumber,
63+
color.GreenString(fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|"))))
64+
internal.AddFlagAndBind(v, fs, fs.Bool, "print-issued-lines", "output.print-issued-lines", true,
65+
color.GreenString("Print lines of code with issue"))
66+
internal.AddFlagAndBind(v, fs, fs.Bool, "print-linter-name", "output.print-linter-name", true,
67+
color.GreenString("Print linter name in issue line"))
68+
internal.AddFlagAndBind(v, fs, fs.Bool, "uniq-by-line", "output.uniq-by-line", true,
69+
color.GreenString("Make issues output unique by line"))
70+
internal.AddFlagAndBind(v, fs, fs.Bool, "sort-results", "output.sort-results", false,
71+
color.GreenString("Sort linter results"))
72+
internal.AddFlagAndBind(v, fs, fs.String, "path-prefix", "output.path-prefix", "",
73+
color.GreenString("Path prefix to add to output"))
74+
}
75+
76+
//nolint:gomnd
77+
func setupIssuesFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
78+
fs.StringSliceP("exclude", "e", nil, color.GreenString("Exclude issue by regexp")) // Hack see Loader.applyStringSliceHack
79+
internal.AddFlagAndBind(v, fs, fs.Bool, "exclude-use-default", "issues.exclude-use-default", true,
80+
getDefaultIssueExcludeHelp())
81+
internal.AddFlagAndBind(v, fs, fs.Bool, "exclude-case-sensitive", "issues.exclude-case-sensitive", false,
82+
color.GreenString("If set to true exclude and exclude rules regular expressions are case-sensitive"))
83+
84+
internal.AddFlagAndBind(v, fs, fs.Int, "max-issues-per-linter", "issues.max-issues-per-linter", 50,
85+
color.GreenString("Maximum issues count per one linter. Set to 0 to disable"))
86+
internal.AddFlagAndBind(v, fs, fs.Int, "max-same-issues", "issues.max-same-issues", 3,
87+
color.GreenString("Maximum count of issues with the same text. Set to 0 to disable"))
88+
89+
const newDesc = "Show only new issues: if there are unstaged changes or untracked files, only those changes " +
90+
"are analyzed, else only changes in HEAD~ are analyzed.\nIt's a super-useful option for integration " +
91+
"of golangci-lint into existing large codebase.\nIt's not practical to fix all existing issues at " +
92+
"the moment of integration: much better to not allow issues in new code.\nFor CI setups, prefer " +
93+
"--new-from-rev=HEAD~, as --new can skip linting the current patch if any scripts generate " +
94+
"unstaged files before golangci-lint runs."
95+
internal.AddFlagAndBindP(v, fs, fs.BoolP, "new", "n", "issues.new", false, color.GreenString(newDesc))
96+
internal.AddFlagAndBind(v, fs, fs.String, "new-from-rev", "issues.new-from-rev", "",
97+
color.GreenString("Show only new issues created after git revision `REV`"))
98+
internal.AddFlagAndBind(v, fs, fs.String, "new-from-patch", "issues.new-from-patch", "",
99+
color.GreenString("Show only new issues created in git patch with file path `PATH`"))
100+
internal.AddFlagAndBind(v, fs, fs.Bool, "whole-files", "issues.whole-files", false,
101+
color.GreenString("Show issues in any part of update files (requires new-from-rev or new-from-patch)"))
102+
internal.AddFlagAndBind(v, fs, fs.Bool, "fix", "issues.fix", false,
103+
color.GreenString("Fix found issues (if it's supported by the linter)"))
104+
}

‎pkg/commands/help.go‎

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,23 @@ import (
88
"github.com/fatih/color"
99
"github.com/spf13/cobra"
1010

11+
"github.com/golangci/golangci-lint/pkg/config"
1112
"github.com/golangci/golangci-lint/pkg/lint/linter"
1213
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
1314
"github.com/golangci/golangci-lint/pkg/logutils"
1415
)
1516

16-
func (e *Executor) initHelp() {
17+
type helpCommand struct {
18+
cmd *cobra.Command
19+
20+
dbManager *lintersdb.Manager
21+
22+
log logutils.Log
23+
}
24+
25+
func newHelpCommand(logger logutils.Log) *helpCommand {
26+
c := &helpCommand{log: logger}
27+
1728
helpCmd := &cobra.Command{
1829
Use: "help",
1930
Short: "Help",
@@ -23,20 +34,31 @@ func (e *Executor) initHelp() {
2334
},
2435
}
2536

26-
helpCmd.AddCommand(&cobra.Command{
27-
Use: "linters",
28-
Short: "Help about linters",
29-
Args: cobra.NoArgs,
30-
ValidArgsFunction: cobra.NoFileCompletions,
31-
Run: e.executeHelp,
32-
})
37+
helpCmd.AddCommand(
38+
&cobra.Command{
39+
Use: "linters",
40+
Short: "Help about linters",
41+
Args: cobra.NoArgs,
42+
ValidArgsFunction: cobra.NoFileCompletions,
43+
Run: c.execute,
44+
PreRun: c.preRun,
45+
},
46+
)
47+
48+
c.cmd = helpCmd
3349

34-
e.rootCmd.SetHelpCommand(helpCmd)
50+
return c
3551
}
3652

37-
func (e *Executor) executeHelp(_ *cobra.Command, _ []string) {
53+
func (c *helpCommand) preRun(_ *cobra.Command, _ []string) {
54+
// The command doesn't depend on the real configuration.
55+
// It just needs the list of all plugins and all presets.
56+
c.dbManager = lintersdb.NewManager(config.NewDefault(), c.log)
57+
}
58+
59+
func (c *helpCommand) execute(_ *cobra.Command, _ []string) {
3860
var enabledLCs, disabledLCs []*linter.Config
39-
for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() {
61+
for _, lc := range c.dbManager.GetAllSupportedLinterConfigs() {
4062
if lc.Internal {
4163
continue
4264
}
@@ -49,13 +71,19 @@ func (e *Executor) executeHelp(_ *cobra.Command, _ []string) {
4971
}
5072

5173
color.Green("Enabled by default linters:\n")
52-
printLinterConfigs(enabledLCs)
74+
printLinters(enabledLCs)
75+
5376
color.Red("\nDisabled by default linters:\n")
54-
printLinterConfigs(disabledLCs)
77+
printLinters(disabledLCs)
5578

5679
color.Green("\nLinters presets:")
80+
c.printPresets()
81+
}
82+
83+
func (c *helpCommand) printPresets() {
5784
for _, p := range lintersdb.AllPresets() {
58-
linters := e.dbManager.GetAllLinterConfigsForPreset(p)
85+
linters := c.dbManager.GetAllLinterConfigsForPreset(p)
86+
5987
var linterNames []string
6088
for _, lc := range linters {
6189
if lc.Internal {
@@ -65,14 +93,16 @@ func (e *Executor) executeHelp(_ *cobra.Command, _ []string) {
6593
linterNames = append(linterNames, lc.Name())
6694
}
6795
sort.Strings(linterNames)
68-
fmt.Fprintf(logutils.StdOut, "%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", "))
96+
97+
_, _ = fmt.Fprintf(logutils.StdOut, "%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", "))
6998
}
7099
}
71100

72-
func printLinterConfigs(lcs []*linter.Config) {
101+
func printLinters(lcs []*linter.Config) {
73102
sort.Slice(lcs, func(i, j int) bool {
74103
return lcs[i].Name() < lcs[j].Name()
75104
})
105+
76106
for _, lc := range lcs {
77107
altNamesStr := ""
78108
if len(lc.AlternativeNames) != 0 {
@@ -91,7 +121,7 @@ func printLinterConfigs(lcs []*linter.Config) {
91121
deprecatedMark = " [" + color.RedString("deprecated") + "]"
92122
}
93123

94-
fmt.Fprintf(logutils.StdOut, "%s%s%s: %s [fast: %t, auto-fix: %t]\n", color.YellowString(lc.Name()),
95-
altNamesStr, deprecatedMark, linterDescription, !lc.IsSlowLinter(), lc.CanAutoFix)
124+
_, _ = fmt.Fprintf(logutils.StdOut, "%s%s%s: %s [fast: %t, auto-fix: %t]\n",
125+
color.YellowString(lc.Name()), altNamesStr, deprecatedMark, linterDescription, !lc.IsSlowLinter(), lc.CanAutoFix)
96126
}
97127
}

‎pkg/commands/internal/vibra.go‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/pflag"
7+
"github.com/spf13/viper"
8+
)
9+
10+
type FlagFunc[T any] func(name string, value T, usage string) *T
11+
12+
type FlagPFunc[T any] func(name, shorthand string, value T, usage string) *T
13+
14+
// AddFlagAndBind adds a Cobra/pflag flag and binds it with Viper.
15+
func AddFlagAndBind[T any](v *viper.Viper, fs *pflag.FlagSet, pfn FlagFunc[T], name, bind string, value T, usage string) {
16+
pfn(name, value, usage)
17+
18+
err := v.BindPFlag(bind, fs.Lookup(name))
19+
if err != nil {
20+
panic(fmt.Sprintf("failed to bind flag %s: %v", name, err))
21+
}
22+
}
23+
24+
// AddFlagAndBindP adds a Cobra/pflag flag and binds it with Viper.
25+
func AddFlagAndBindP[T any](v *viper.Viper, fs *pflag.FlagSet, pfn FlagPFunc[T], name, shorthand, bind string, value T, usage string) {
26+
pfn(name, shorthand, value, usage)
27+
28+
err := v.BindPFlag(bind, fs.Lookup(name))
29+
if err != nil {
30+
panic(fmt.Sprintf("failed to bind flag %s: %v", name, err))
31+
}
32+
}

‎pkg/commands/linters.go‎

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,86 @@ package commands
22

33
import (
44
"fmt"
5-
"strings"
65

76
"github.com/fatih/color"
87
"github.com/spf13/cobra"
9-
"github.com/spf13/pflag"
8+
"github.com/spf13/viper"
109

1110
"github.com/golangci/golangci-lint/pkg/config"
1211
"github.com/golangci/golangci-lint/pkg/lint/linter"
1312
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
13+
"github.com/golangci/golangci-lint/pkg/logutils"
1414
)
1515

16-
func (e *Executor) initLinters() {
16+
type lintersOptions struct {
17+
config.LoaderOptions
18+
}
19+
20+
type lintersCommand struct {
21+
viper *viper.Viper
22+
cmd *cobra.Command
23+
24+
opts lintersOptions
25+
26+
cfg *config.Config
27+
28+
log logutils.Log
29+
30+
dbManager *lintersdb.Manager
31+
enabledLintersSet *lintersdb.EnabledSet
32+
}
33+
34+
func newLintersCommand(logger logutils.Log, cfg *config.Config) *lintersCommand {
35+
c := &lintersCommand{
36+
viper: viper.New(),
37+
cfg: cfg,
38+
log: logger,
39+
}
40+
1741
lintersCmd := &cobra.Command{
1842
Use: "linters",
1943
Short: "List current linters configuration",
2044
Args: cobra.NoArgs,
2145
ValidArgsFunction: cobra.NoFileCompletions,
22-
RunE: e.executeLinters,
46+
RunE: c.execute,
47+
PreRunE: c.preRunE,
2348
}
2449

2550
fs := lintersCmd.Flags()
2651
fs.SortFlags = false // sort them as they are defined here
2752

28-
initConfigFileFlagSet(fs, &e.cfg.Run)
29-
initLintersFlagSet(fs, &e.cfg.Linters)
53+
setupConfigFileFlagSet(fs, &c.opts.LoaderOptions)
54+
setupLintersFlagSet(c.viper, fs)
3055

31-
e.rootCmd.AddCommand(lintersCmd)
56+
c.cmd = lintersCmd
3257

33-
e.lintersCmd = lintersCmd
58+
return c
3459
}
3560

36-
// executeLinters runs the 'linters' CLI command, which displays the supported linters.
37-
func (e *Executor) executeLinters(_ *cobra.Command, _ []string) error {
38-
enabledLintersMap, err := e.enabledLintersSet.GetEnabledLintersMap()
61+
func (c *lintersCommand) preRunE(cmd *cobra.Command, _ []string) error {
62+
loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), c.opts.LoaderOptions, c.cfg)
63+
64+
if err := loader.Load(); err != nil {
65+
return fmt.Errorf("can't load config: %w", err)
66+
}
67+
68+
c.dbManager = lintersdb.NewManager(c.cfg, c.log)
69+
c.enabledLintersSet = lintersdb.NewEnabledSet(c.dbManager,
70+
lintersdb.NewValidator(c.dbManager), c.log.Child(logutils.DebugKeyLintersDB), c.cfg)
71+
72+
return nil
73+
}
74+
75+
func (c *lintersCommand) execute(_ *cobra.Command, _ []string) error {
76+
enabledLintersMap, err := c.enabledLintersSet.GetEnabledLintersMap()
3977
if err != nil {
4078
return fmt.Errorf("can't get enabled linters: %w", err)
4179
}
4280

4381
var enabledLinters []*linter.Config
4482
var disabledLCs []*linter.Config
4583

46-
for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() {
84+
for _, lc := range c.dbManager.GetAllSupportedLinterConfigs() {
4785
if lc.Internal {
4886
continue
4987
}
@@ -56,20 +94,9 @@ func (e *Executor) executeLinters(_ *cobra.Command, _ []string) error {
5694
}
5795

5896
color.Green("Enabled by your configuration linters:\n")
59-
printLinterConfigs(enabledLinters)
97+
printLinters(enabledLinters)
6098
color.Red("\nDisabled by your configuration linters:\n")
61-
printLinterConfigs(disabledLCs)
99+
printLinters(disabledLCs)
62100

63101
return nil
64102
}
65-
66-
func initLintersFlagSet(fs *pflag.FlagSet, cfg *config.Linters) {
67-
fs.StringSliceVarP(&cfg.Disable, "disable", "D", nil, wh("Disable specific linter"))
68-
fs.BoolVar(&cfg.DisableAll, "disable-all", false, wh("Disable all linters"))
69-
fs.StringSliceVarP(&cfg.Enable, "enable", "E", nil, wh("Enable specific linter"))
70-
fs.BoolVar(&cfg.EnableAll, "enable-all", false, wh("Enable all linters"))
71-
fs.BoolVar(&cfg.Fast, "fast", false, wh("Enable only fast linters from enabled linters set (first run won't be fast)"))
72-
fs.StringSliceVarP(&cfg.Presets, "presets", "p", nil,
73-
wh(fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint help linters' to see "+
74-
"them. This option implies option --disable-all", strings.Join(lintersdb.AllPresets(), "|"))))
75-
}

‎pkg/commands/root.go‎

Lines changed: 112 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,168 @@
11
package commands
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
6-
"runtime"
7-
"runtime/pprof"
8-
"runtime/trace"
9-
"strconv"
7+
"slices"
108

9+
"github.com/fatih/color"
1110
"github.com/spf13/cobra"
1211
"github.com/spf13/pflag"
1312

1413
"github.com/golangci/golangci-lint/pkg/config"
15-
"github.com/golangci/golangci-lint/pkg/exitcodes"
1614
"github.com/golangci/golangci-lint/pkg/logutils"
15+
"github.com/golangci/golangci-lint/pkg/report"
1716
)
1817

19-
const (
20-
// envHelpRun value: "1".
21-
envHelpRun = "HELP_RUN"
22-
envMemProfileRate = "GL_MEM_PROFILE_RATE"
23-
)
18+
func Execute(info BuildInfo) error {
19+
return newRootCommand(info).Execute()
20+
}
21+
22+
type rootOptions struct {
23+
PrintVersion bool // Flag only.
24+
25+
Verbose bool // Flag only.
26+
Color string // Flag only.
27+
}
28+
29+
type rootCommand struct {
30+
cmd *cobra.Command
31+
opts rootOptions
32+
33+
log logutils.Log
34+
}
35+
36+
func newRootCommand(info BuildInfo) *rootCommand {
37+
c := &rootCommand{}
2438

25-
func (e *Executor) initRoot() {
2639
rootCmd := &cobra.Command{
2740
Use: "golangci-lint",
2841
Short: "golangci-lint is a smart linters runner.",
2942
Long: `Smart, fast linters runner.`,
3043
Args: cobra.NoArgs,
3144
RunE: func(cmd *cobra.Command, _ []string) error {
45+
if c.opts.PrintVersion {
46+
_ = printVersion(logutils.StdOut, info)
47+
return nil
48+
}
49+
3250
return cmd.Help()
3351
},
34-
PersistentPreRunE: e.persistentPreRun,
35-
PersistentPostRunE: e.persistentPostRun,
3652
}
3753

38-
initRootFlagSet(rootCmd.PersistentFlags(), e.cfg)
39-
40-
e.rootCmd = rootCmd
41-
}
54+
fs := rootCmd.Flags()
55+
fs.BoolVar(&c.opts.PrintVersion, "version", false, color.GreenString("Print version"))
4256

43-
func (e *Executor) persistentPreRun(_ *cobra.Command, _ []string) error {
44-
if e.cfg.Run.PrintVersion {
45-
_ = printVersion(logutils.StdOut, e.buildInfo)
46-
os.Exit(exitcodes.Success) // a return nil is not enough to stop the process because we are inside the `preRun`.
47-
}
57+
setupRootPersistentFlags(rootCmd.PersistentFlags(), &c.opts)
4858

49-
runtime.GOMAXPROCS(e.cfg.Run.Concurrency)
59+
reportData := &report.Data{}
60+
log := report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), reportData)
5061

51-
if e.cfg.Run.CPUProfilePath != "" {
52-
f, err := os.Create(e.cfg.Run.CPUProfilePath)
53-
if err != nil {
54-
return fmt.Errorf("can't create file %s: %w", e.cfg.Run.CPUProfilePath, err)
55-
}
56-
if err := pprof.StartCPUProfile(f); err != nil {
57-
return fmt.Errorf("can't start CPU profiling: %w", err)
58-
}
59-
}
62+
// Dedicated configuration for each command to avoid side effects of bindings.
63+
rootCmd.AddCommand(
64+
newLintersCommand(log, config.NewDefault()).cmd,
65+
newRunCommand(log, config.NewDefault(), reportData, info).cmd,
66+
newCacheCommand().cmd,
67+
newConfigCommand(log).cmd,
68+
newVersionCommand(info).cmd,
69+
)
6070

61-
if e.cfg.Run.MemProfilePath != "" {
62-
if rate := os.Getenv(envMemProfileRate); rate != "" {
63-
runtime.MemProfileRate, _ = strconv.Atoi(rate)
64-
}
65-
}
71+
rootCmd.SetHelpCommand(newHelpCommand(log).cmd)
6672

67-
if e.cfg.Run.TracePath != "" {
68-
f, err := os.Create(e.cfg.Run.TracePath)
69-
if err != nil {
70-
return fmt.Errorf("can't create file %s: %w", e.cfg.Run.TracePath, err)
71-
}
72-
if err = trace.Start(f); err != nil {
73-
return fmt.Errorf("can't start tracing: %w", err)
74-
}
75-
}
73+
c.log = log
74+
c.cmd = rootCmd
7675

77-
return nil
76+
return c
7877
}
7978

80-
func (e *Executor) persistentPostRun(_ *cobra.Command, _ []string) error {
81-
if e.cfg.Run.CPUProfilePath != "" {
82-
pprof.StopCPUProfile()
79+
func (c *rootCommand) Execute() error {
80+
err := setupLogger(c.log)
81+
if err != nil {
82+
return err
8383
}
8484

85-
if e.cfg.Run.MemProfilePath != "" {
86-
f, err := os.Create(e.cfg.Run.MemProfilePath)
87-
if err != nil {
88-
return fmt.Errorf("can't create file %s: %w", e.cfg.Run.MemProfilePath, err)
89-
}
85+
return c.cmd.Execute()
86+
}
9087

91-
var ms runtime.MemStats
92-
runtime.ReadMemStats(&ms)
93-
printMemStats(&ms, e.log)
88+
func setupRootPersistentFlags(fs *pflag.FlagSet, opts *rootOptions) {
89+
fs.BoolVarP(&opts.Verbose, "verbose", "v", false, color.GreenString("Verbose output"))
90+
fs.StringVar(&opts.Color, "color", "auto", color.GreenString("Use color when printing; can be 'always', 'auto', or 'never'"))
91+
}
9492

95-
if err := pprof.WriteHeapProfile(f); err != nil {
96-
return fmt.Errorf("can't write heap profile: %w", err)
97-
}
98-
_ = f.Close()
93+
func setupLogger(logger logutils.Log) error {
94+
opts, err := forceRootParsePersistentFlags()
95+
if err != nil && !errors.Is(err, pflag.ErrHelp) {
96+
return err
9997
}
10098

101-
if e.cfg.Run.TracePath != "" {
102-
trace.Stop()
99+
if opts == nil {
100+
return nil
103101
}
104102

105-
os.Exit(e.exitCode)
103+
logutils.SetupVerboseLog(logger, opts.Verbose)
104+
105+
switch opts.Color {
106+
case "always":
107+
color.NoColor = false
108+
case "never":
109+
color.NoColor = true
110+
case "auto":
111+
// nothing
112+
default:
113+
logger.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", opts.Color)
114+
}
106115

107116
return nil
108117
}
109118

110-
func initRootFlagSet(fs *pflag.FlagSet, cfg *config.Config) {
111-
fs.BoolVarP(&cfg.Run.IsVerbose, "verbose", "v", false, wh("Verbose output"))
112-
fs.StringVar(&cfg.Output.Color, "color", "auto", wh("Use color when printing; can be 'always', 'auto', or 'never'"))
119+
func forceRootParsePersistentFlags() (*rootOptions, error) {
120+
// We use another pflag.FlagSet here to not set `changed` flag on cmd.Flags() options.
121+
// Otherwise, string slice options will be duplicated.
122+
fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError)
113123

114-
fs.StringVar(&cfg.Run.CPUProfilePath, "cpu-profile-path", "", wh("Path to CPU profile output file"))
115-
fs.StringVar(&cfg.Run.MemProfilePath, "mem-profile-path", "", wh("Path to memory profile output file"))
116-
fs.StringVar(&cfg.Run.TracePath, "trace-path", "", wh("Path to trace output file"))
124+
// Ignore unknown flags because we will parse the command flags later.
125+
fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
117126

118-
fs.IntVarP(&cfg.Run.Concurrency, "concurrency", "j", getDefaultConcurrency(),
119-
wh("Number of CPUs to use (Default: number of logical CPUs)"))
127+
opts := &rootOptions{}
120128

121-
fs.BoolVar(&cfg.Run.PrintVersion, "version", false, wh("Print version"))
122-
}
129+
// Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations:
130+
// `changed` variable inside string slice vars will be shared.
131+
// Use another config variable here,
132+
// to not affect main parsing by this parsing of only config option.
133+
setupRootPersistentFlags(fs, opts)
123134

124-
func printMemStats(ms *runtime.MemStats, logger logutils.Log) {
125-
logger.Infof("Mem stats: alloc=%s total_alloc=%s sys=%s "+
126-
"heap_alloc=%s heap_sys=%s heap_idle=%s heap_released=%s heap_in_use=%s "+
127-
"stack_in_use=%s stack_sys=%s "+
128-
"mspan_sys=%s mcache_sys=%s buck_hash_sys=%s gc_sys=%s other_sys=%s "+
129-
"mallocs_n=%d frees_n=%d heap_objects_n=%d gc_cpu_fraction=%.2f",
130-
formatMemory(ms.Alloc), formatMemory(ms.TotalAlloc), formatMemory(ms.Sys),
131-
formatMemory(ms.HeapAlloc), formatMemory(ms.HeapSys),
132-
formatMemory(ms.HeapIdle), formatMemory(ms.HeapReleased), formatMemory(ms.HeapInuse),
133-
formatMemory(ms.StackInuse), formatMemory(ms.StackSys),
134-
formatMemory(ms.MSpanSys), formatMemory(ms.MCacheSys), formatMemory(ms.BuckHashSys),
135-
formatMemory(ms.GCSys), formatMemory(ms.OtherSys),
136-
ms.Mallocs, ms.Frees, ms.HeapObjects, ms.GCCPUFraction)
137-
}
135+
fs.Usage = func() {} // otherwise, help text will be printed twice
138136

139-
func formatMemory(memBytes uint64) string {
140-
const Kb = 1024
141-
const Mb = Kb * 1024
137+
if err := fs.Parse(safeArgs(fs, os.Args)); err != nil {
138+
if errors.Is(err, pflag.ErrHelp) {
139+
return nil, err
140+
}
142141

143-
if memBytes < Kb {
144-
return fmt.Sprintf("%db", memBytes)
142+
return nil, fmt.Errorf("can't parse args: %w", err)
145143
}
146-
if memBytes < Mb {
147-
return fmt.Sprintf("%dkb", memBytes/Kb)
148-
}
149-
return fmt.Sprintf("%dmb", memBytes/Mb)
144+
145+
return opts, nil
150146
}
151147

152-
func getDefaultConcurrency() int {
153-
if os.Getenv(envHelpRun) == "1" {
154-
// Make stable concurrency for generating help documentation.
155-
const prettyConcurrency = 8
156-
return prettyConcurrency
148+
// Shorthands are a problem because pflag, with UnknownFlags, will try to parse all the letters as options.
149+
// A shorthand can aggregate several letters (ex `ps -aux`)
150+
// The function replaces non-supported shorthands by a dumb flag.
151+
func safeArgs(fs *pflag.FlagSet, args []string) []string {
152+
var shorthands []string
153+
fs.VisitAll(func(flag *pflag.Flag) {
154+
shorthands = append(shorthands, flag.Shorthand)
155+
})
156+
157+
var cleanArgs []string
158+
for _, arg := range args {
159+
if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' && !slices.Contains(shorthands, string(arg[1])) {
160+
cleanArgs = append(cleanArgs, "--potato")
161+
continue
162+
}
163+
164+
cleanArgs = append(cleanArgs, arg)
157165
}
158166

159-
return runtime.NumCPU()
167+
return cleanArgs
160168
}

‎pkg/commands/run.go‎

Lines changed: 398 additions & 172 deletions
Large diffs are not rendered by default.

‎pkg/commands/version.go‎

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ import (
88
"runtime/debug"
99
"strings"
1010

11+
"github.com/fatih/color"
1112
"github.com/spf13/cobra"
12-
"github.com/spf13/pflag"
13-
14-
"github.com/golangci/golangci-lint/pkg/config"
1513
)
1614

1715
type BuildInfo struct {
@@ -26,65 +24,75 @@ type versionInfo struct {
2624
BuildInfo *debug.BuildInfo
2725
}
2826

29-
func (e *Executor) initVersion() {
27+
type versionOptions struct {
28+
Format string `mapstructure:"format"`
29+
Debug bool `mapstructure:"debug"`
30+
}
31+
32+
type versionCommand struct {
33+
cmd *cobra.Command
34+
opts versionOptions
35+
36+
info BuildInfo
37+
}
38+
39+
func newVersionCommand(info BuildInfo) *versionCommand {
40+
c := &versionCommand{info: info}
41+
3042
versionCmd := &cobra.Command{
3143
Use: "version",
3244
Short: "Version",
3345
Args: cobra.NoArgs,
3446
ValidArgsFunction: cobra.NoFileCompletions,
35-
RunE: e.executeVersion,
47+
RunE: c.execute,
3648
}
3749

3850
fs := versionCmd.Flags()
3951
fs.SortFlags = false // sort them as they are defined here
4052

41-
initVersionFlagSet(fs, e.cfg)
53+
fs.StringVar(&c.opts.Format, "format", "", color.GreenString("The version's format can be: 'short', 'json'"))
54+
fs.BoolVar(&c.opts.Debug, "debug", false, color.GreenString("Add build information"))
55+
56+
c.cmd = versionCmd
4257

43-
e.rootCmd.AddCommand(versionCmd)
58+
return c
4459
}
4560

46-
func (e *Executor) executeVersion(_ *cobra.Command, _ []string) error {
47-
if e.cfg.Version.Debug {
61+
func (c *versionCommand) execute(_ *cobra.Command, _ []string) error {
62+
if c.opts.Debug {
4863
info, ok := debug.ReadBuildInfo()
4964
if !ok {
5065
return nil
5166
}
5267

53-
switch strings.ToLower(e.cfg.Version.Format) {
68+
switch strings.ToLower(c.opts.Format) {
5469
case "json":
5570
return json.NewEncoder(os.Stdout).Encode(versionInfo{
56-
Info: e.buildInfo,
71+
Info: c.info,
5772
BuildInfo: info,
5873
})
5974

6075
default:
6176
fmt.Println(info.String())
62-
return printVersion(os.Stdout, e.buildInfo)
77+
return printVersion(os.Stdout, c.info)
6378
}
6479
}
6580

66-
switch strings.ToLower(e.cfg.Version.Format) {
81+
switch strings.ToLower(c.opts.Format) {
6782
case "short":
68-
fmt.Println(e.buildInfo.Version)
83+
fmt.Println(c.info.Version)
6984
return nil
7085

7186
case "json":
72-
return json.NewEncoder(os.Stdout).Encode(e.buildInfo)
87+
return json.NewEncoder(os.Stdout).Encode(c.info)
7388

7489
default:
75-
return printVersion(os.Stdout, e.buildInfo)
90+
return printVersion(os.Stdout, c.info)
7691
}
7792
}
7893

79-
func initVersionFlagSet(fs *pflag.FlagSet, cfg *config.Config) {
80-
// Version config
81-
vc := &cfg.Version
82-
fs.StringVar(&vc.Format, "format", "", wh("The version's format can be: 'short', 'json'"))
83-
fs.BoolVar(&vc.Debug, "debug", false, wh("Add build information"))
84-
}
85-
86-
func printVersion(w io.Writer, buildInfo BuildInfo) error {
94+
func printVersion(w io.Writer, info BuildInfo) error {
8795
_, err := fmt.Fprintf(w, "golangci-lint has version %s built with %s from %s on %s\n",
88-
buildInfo.Version, buildInfo.GoVersion, buildInfo.Commit, buildInfo.Date)
96+
info.Version, info.GoVersion, info.Commit, info.Date)
8997
return err
9098
}

‎pkg/config/config.go‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package config
22

33
import (
4+
"errors"
5+
"fmt"
46
"os"
57
"strings"
68

@@ -21,8 +23,6 @@ type Config struct {
2123
Issues Issues `mapstructure:"issues"`
2224
Severity Severity `mapstructure:"severity"`
2325

24-
Version Version // Flag only. // TODO(ldez) only used by the version command.
25-
2626
InternalCmdTest bool // Option is used only for testing golangci-lint command, don't use it
2727
InternalTest bool // Option is used only for testing golangci-lint code, don't use it
2828
}
@@ -32,6 +32,25 @@ func (c *Config) GetConfigDir() string {
3232
return c.cfgDir
3333
}
3434

35+
func (c *Config) Validate() error {
36+
for i, rule := range c.Issues.ExcludeRules {
37+
if err := rule.Validate(); err != nil {
38+
return fmt.Errorf("error in exclude rule #%d: %w", i, err)
39+
}
40+
}
41+
42+
if len(c.Severity.Rules) > 0 && c.Severity.Default == "" {
43+
return errors.New("can't set severity rule option: no default severity defined")
44+
}
45+
for i, rule := range c.Severity.Rules {
46+
if err := rule.Validate(); err != nil {
47+
return fmt.Errorf("error in severity rule #%d: %w", i, err)
48+
}
49+
}
50+
51+
return nil
52+
}
53+
3554
func NewDefault() *Config {
3655
return &Config{
3756
LintersSettings: defaultLintersSettings,
@@ -57,7 +76,7 @@ func IsGoGreaterThanOrEqual(current, limit string) bool {
5776
return v1.GreaterThanOrEqual(l)
5877
}
5978

60-
func DetectGoVersion() string {
79+
func detectGoVersion() string {
6180
file, _ := gomoddirectives.GetModuleFile()
6281

6382
if file != nil && file.Go != nil && file.Go.Version != "" {

‎pkg/config/loader.go‎

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
package config
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"slices"
9+
"strings"
10+
11+
"github.com/go-viper/mapstructure/v2"
12+
"github.com/mitchellh/go-homedir"
13+
"github.com/spf13/pflag"
14+
"github.com/spf13/viper"
15+
16+
"github.com/golangci/golangci-lint/pkg/exitcodes"
17+
"github.com/golangci/golangci-lint/pkg/fsutils"
18+
"github.com/golangci/golangci-lint/pkg/logutils"
19+
)
20+
21+
var errConfigDisabled = errors.New("config is disabled by --no-config")
22+
23+
type LoaderOptions struct {
24+
Config string // Flag only. The path to the golangci config file, as specified with the --config argument.
25+
NoConfig bool // Flag only.
26+
}
27+
28+
type Loader struct {
29+
opts LoaderOptions
30+
31+
viper *viper.Viper
32+
fs *pflag.FlagSet
33+
34+
log logutils.Log
35+
36+
cfg *Config
37+
}
38+
39+
func NewLoader(log logutils.Log, v *viper.Viper, fs *pflag.FlagSet, opts LoaderOptions, cfg *Config) *Loader {
40+
return &Loader{
41+
opts: opts,
42+
viper: v,
43+
fs: fs,
44+
log: log,
45+
cfg: cfg,
46+
}
47+
}
48+
49+
func (l *Loader) Load() error {
50+
err := l.setConfigFile()
51+
if err != nil {
52+
return err
53+
}
54+
55+
err = l.parseConfig()
56+
if err != nil {
57+
return err
58+
}
59+
60+
l.applyStringSliceHack()
61+
62+
if l.cfg.Run.Go == "" {
63+
l.cfg.Run.Go = detectGoVersion()
64+
}
65+
66+
return nil
67+
}
68+
69+
func (l *Loader) setConfigFile() error {
70+
configFile, err := l.evaluateOptions()
71+
if err != nil {
72+
if errors.Is(err, errConfigDisabled) {
73+
return nil
74+
}
75+
76+
return fmt.Errorf("can't parse --config option: %w", err)
77+
}
78+
79+
if configFile != "" {
80+
l.viper.SetConfigFile(configFile)
81+
82+
// Assume YAML if the file has no extension.
83+
if filepath.Ext(configFile) == "" {
84+
l.viper.SetConfigType("yaml")
85+
}
86+
} else {
87+
l.setupConfigFileSearch()
88+
}
89+
90+
return nil
91+
}
92+
93+
func (l *Loader) evaluateOptions() (string, error) {
94+
if l.opts.NoConfig && l.opts.Config != "" {
95+
return "", errors.New("can't combine option --config and --no-config")
96+
}
97+
98+
if l.opts.NoConfig {
99+
return "", errConfigDisabled
100+
}
101+
102+
configFile, err := homedir.Expand(l.opts.Config)
103+
if err != nil {
104+
return "", errors.New("failed to expand configuration path")
105+
}
106+
107+
return configFile, nil
108+
}
109+
110+
func (l *Loader) setupConfigFileSearch() {
111+
firstArg := extractFirstPathArg()
112+
113+
absStartPath, err := filepath.Abs(firstArg)
114+
if err != nil {
115+
l.log.Warnf("Can't make abs path for %q: %s", firstArg, err)
116+
absStartPath = filepath.Clean(firstArg)
117+
}
118+
119+
// start from it
120+
var curDir string
121+
if fsutils.IsDir(absStartPath) {
122+
curDir = absStartPath
123+
} else {
124+
curDir = filepath.Dir(absStartPath)
125+
}
126+
127+
// find all dirs from it up to the root
128+
configSearchPaths := []string{"./"}
129+
130+
for {
131+
configSearchPaths = append(configSearchPaths, curDir)
132+
133+
newCurDir := filepath.Dir(curDir)
134+
if curDir == newCurDir || newCurDir == "" {
135+
break
136+
}
137+
138+
curDir = newCurDir
139+
}
140+
141+
// find home directory for global config
142+
if home, err := homedir.Dir(); err != nil {
143+
l.log.Warnf("Can't get user's home directory: %s", err.Error())
144+
} else if !slices.Contains(configSearchPaths, home) {
145+
configSearchPaths = append(configSearchPaths, home)
146+
}
147+
148+
l.log.Infof("Config search paths: %s", configSearchPaths)
149+
150+
l.viper.SetConfigName(".golangci")
151+
152+
for _, p := range configSearchPaths {
153+
l.viper.AddConfigPath(p)
154+
}
155+
}
156+
157+
func (l *Loader) parseConfig() error {
158+
if err := l.viper.ReadInConfig(); err != nil {
159+
var configFileNotFoundError viper.ConfigFileNotFoundError
160+
if errors.As(err, &configFileNotFoundError) {
161+
// Load configuration from flags only.
162+
err = l.viper.Unmarshal(l.cfg)
163+
if err != nil {
164+
return err
165+
}
166+
167+
if err = l.cfg.Validate(); err != nil {
168+
return fmt.Errorf("can't validate config: %w", err)
169+
}
170+
171+
return nil
172+
}
173+
174+
return fmt.Errorf("can't read viper config: %w", err)
175+
}
176+
177+
err := l.setConfigDir()
178+
if err != nil {
179+
return err
180+
}
181+
182+
// Load configuration from all sources (flags, file).
183+
if err := l.viper.Unmarshal(l.cfg, fileDecoderHook()); err != nil {
184+
return fmt.Errorf("can't unmarshal config by viper: %w", err)
185+
}
186+
187+
if err := l.cfg.Validate(); err != nil {
188+
return fmt.Errorf("can't validate config: %w", err)
189+
}
190+
191+
if l.cfg.InternalTest { // just for testing purposes: to detect config file usage
192+
_, _ = fmt.Fprintln(logutils.StdOut, "test")
193+
os.Exit(exitcodes.Success)
194+
}
195+
196+
return nil
197+
}
198+
199+
func (l *Loader) setConfigDir() error {
200+
usedConfigFile := l.viper.ConfigFileUsed()
201+
if usedConfigFile == "" {
202+
return nil
203+
}
204+
205+
if usedConfigFile == os.Stdin.Name() {
206+
usedConfigFile = ""
207+
l.log.Infof("Reading config file stdin")
208+
} else {
209+
var err error
210+
usedConfigFile, err = fsutils.ShortestRelPath(usedConfigFile, "")
211+
if err != nil {
212+
l.log.Warnf("Can't pretty print config file path: %v", err)
213+
}
214+
215+
l.log.Infof("Used config file %s", usedConfigFile)
216+
}
217+
218+
usedConfigDir, err := filepath.Abs(filepath.Dir(usedConfigFile))
219+
if err != nil {
220+
return errors.New("can't get config directory")
221+
}
222+
223+
l.cfg.cfgDir = usedConfigDir
224+
225+
return nil
226+
}
227+
228+
// Hack to append values from StringSlice flags.
229+
// Viper always overrides StringSlice values.
230+
// https://github.com/spf13/viper/issues/1448
231+
// So StringSlice flags are not bind to Viper like that their values are obtain via Cobra Flags.
232+
func (l *Loader) applyStringSliceHack() {
233+
if l.fs == nil {
234+
return
235+
}
236+
237+
l.appendStringSlice("enable", &l.cfg.Linters.Enable)
238+
l.appendStringSlice("disable", &l.cfg.Linters.Disable)
239+
l.appendStringSlice("presets", &l.cfg.Linters.Presets)
240+
l.appendStringSlice("build-tags", &l.cfg.Run.BuildTags)
241+
l.appendStringSlice("skip-dirs", &l.cfg.Run.SkipDirs)
242+
l.appendStringSlice("skip-files", &l.cfg.Run.SkipFiles)
243+
l.appendStringSlice("exclude", &l.cfg.Issues.ExcludePatterns)
244+
}
245+
246+
func (l *Loader) appendStringSlice(name string, current *[]string) {
247+
if l.fs.Changed(name) {
248+
val, _ := l.fs.GetStringSlice(name)
249+
*current = append(*current, val...)
250+
}
251+
}
252+
253+
func fileDecoderHook() viper.DecoderConfigOption {
254+
return viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
255+
// Default hooks (https://github.com/spf13/viper/blob/518241257478c557633ab36e474dfcaeb9a3c623/viper.go#L135-L138).
256+
mapstructure.StringToTimeDurationHookFunc(),
257+
mapstructure.StringToSliceHookFunc(","),
258+
259+
// Needed for forbidigo.
260+
mapstructure.TextUnmarshallerHookFunc(),
261+
))
262+
}
263+
264+
func extractFirstPathArg() string {
265+
args := os.Args
266+
267+
// skip all args ([golangci-lint, run/linters]) before files/dirs list
268+
for len(args) != 0 {
269+
if args[0] == "run" {
270+
args = args[1:]
271+
break
272+
}
273+
274+
args = args[1:]
275+
}
276+
277+
// find first file/dir arg
278+
firstArg := "./..."
279+
for _, arg := range args {
280+
if !strings.HasPrefix(arg, "-") {
281+
firstArg = arg
282+
break
283+
}
284+
}
285+
286+
return firstArg
287+
}

‎pkg/config/output.go‎

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,10 @@ var OutFormats = []string{
2828
}
2929

3030
type Output struct {
31-
Format string `mapstructure:"format"`
32-
PrintIssuedLine bool `mapstructure:"print-issued-lines"`
33-
PrintLinterName bool `mapstructure:"print-linter-name"`
34-
UniqByLine bool `mapstructure:"uniq-by-line"`
35-
SortResults bool `mapstructure:"sort-results"`
36-
PrintWelcomeMessage bool `mapstructure:"print-welcome"`
37-
PathPrefix string `mapstructure:"path-prefix"`
38-
39-
// only work with CLI flags because the setup of logs is done before the config file parsing.
40-
Color string // Flag only.
31+
Format string `mapstructure:"format"`
32+
PrintIssuedLine bool `mapstructure:"print-issued-lines"`
33+
PrintLinterName bool `mapstructure:"print-linter-name"`
34+
UniqByLine bool `mapstructure:"uniq-by-line"`
35+
SortResults bool `mapstructure:"sort-results"`
36+
PathPrefix string `mapstructure:"path-prefix"`
4137
}

‎pkg/config/reader.go‎

Lines changed: 0 additions & 248 deletions
This file was deleted.

‎pkg/config/run.go‎

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,6 @@ type Run struct {
2525

2626
ShowStats bool `mapstructure:"show-stats"`
2727

28-
// --- Flags only section.
29-
30-
IsVerbose bool `mapstructure:"verbose"` // Flag only
31-
32-
PrintVersion bool // Flag only. (used by the root command)
33-
34-
CPUProfilePath string // Flag only.
35-
MemProfilePath string // Flag only.
36-
TracePath string // Flag only.
37-
38-
PrintResourcesUsage bool `mapstructure:"print-resources-usage"` // Flag only. // TODO(ldez) need to be enforced.
39-
40-
Config string // Flag only. The path to the golangci config file, as specified with the --config argument.
41-
NoConfig bool // Flag only.
42-
43-
Args []string // Flag only. // TODO(ldez) identify the real need and usage.
28+
// It's obtain by flags and use for the tests and the context loader.
29+
Args []string // Internal needs.
4430
}

‎pkg/lint/lintersdb/custom_linters.go‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"path/filepath"
77
"plugin"
88

9-
"github.com/spf13/viper"
109
"golang.org/x/tools/go/analysis"
1110

1211
"github.com/golangci/golangci-lint/pkg/config"
@@ -67,12 +66,7 @@ func (m *Manager) loadCustomLinterConfig(name string, settings config.CustomLint
6766
func (m *Manager) getAnalyzerPlugin(path string, settings any) ([]*analysis.Analyzer, error) {
6867
if !filepath.IsAbs(path) {
6968
// resolve non-absolute paths relative to config file's directory
70-
configFilePath := viper.ConfigFileUsed()
71-
absConfigFilePath, err := filepath.Abs(configFilePath)
72-
if err != nil {
73-
return nil, fmt.Errorf("could not get absolute representation of config file path %q: %w", configFilePath, err)
74-
}
75-
path = filepath.Join(filepath.Dir(absConfigFilePath), path)
69+
path = filepath.Join(m.cfg.GetConfigDir(), path)
7670
}
7771

7872
plug, err := plugin.Open(path)

‎pkg/logutils/logutils.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,15 @@ func HaveDebugTag(tag string) bool {
9999
return enabledDebugs[tag]
100100
}
101101

102+
var verbose bool
103+
102104
func SetupVerboseLog(log Log, isVerbose bool) {
103105
if isVerbose {
106+
verbose = isVerbose
104107
log.SetLevel(LogLevelInfo)
105108
}
106109
}
110+
111+
func IsVerbose() bool {
112+
return verbose
113+
}

‎test/run_test.go‎

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -536,86 +536,6 @@ func TestAbsPathFileAnalysis(t *testing.T) {
536536
ExpectHasIssue("indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)")
537537
}
538538

539-
func TestDisallowedOptionsInConfig(t *testing.T) {
540-
cases := []struct {
541-
cfg string
542-
option string
543-
}{
544-
{
545-
cfg: `
546-
ruN:
547-
Args:
548-
- 1
549-
`,
550-
},
551-
{
552-
cfg: `
553-
run:
554-
CPUProfilePath: path
555-
`,
556-
option: "--cpu-profile-path=path",
557-
},
558-
{
559-
cfg: `
560-
run:
561-
MemProfilePath: path
562-
`,
563-
option: "--mem-profile-path=path",
564-
},
565-
{
566-
cfg: `
567-
run:
568-
TracePath: path
569-
`,
570-
option: "--trace-path=path",
571-
},
572-
{
573-
cfg: `
574-
run:
575-
Verbose: true
576-
`,
577-
option: "-v",
578-
},
579-
}
580-
581-
testshared.InstallGolangciLint(t)
582-
583-
for _, c := range cases {
584-
// Run with disallowed option set only in config
585-
testshared.NewRunnerBuilder(t).
586-
WithConfig(c.cfg).
587-
WithTargetPath(testdataDir, minimalPkg).
588-
Runner().
589-
Run().
590-
ExpectExitCode(exitcodes.Failure)
591-
592-
if c.option == "" {
593-
continue
594-
}
595-
596-
args := []string{c.option, "--fast"}
597-
598-
// Run with disallowed option set only in command-line
599-
testshared.NewRunnerBuilder(t).
600-
WithNoConfig().
601-
WithArgs(args...).
602-
WithTargetPath(testdataDir, minimalPkg).
603-
Runner().
604-
Run().
605-
ExpectExitCode(exitcodes.Success)
606-
607-
// Run with disallowed option set both in command-line and in config
608-
609-
testshared.NewRunnerBuilder(t).
610-
WithConfig(c.cfg).
611-
WithArgs(args...).
612-
WithTargetPath(testdataDir, minimalPkg).
613-
Runner().
614-
Run().
615-
ExpectExitCode(exitcodes.Failure)
616-
}
617-
}
618-
619539
func TestPathPrefix(t *testing.T) {
620540
testCases := []struct {
621541
desc string

0 commit comments

Comments
 (0)
Please sign in to comment.