| 
1 | 1 | package commands  | 
2 | 2 | 
 
  | 
3 | 3 | import (  | 
 | 4 | +	"errors"  | 
4 | 5 | 	"fmt"  | 
5 | 6 | 	"os"  | 
6 |  | -	"runtime"  | 
7 |  | -	"runtime/pprof"  | 
8 |  | -	"runtime/trace"  | 
9 |  | -	"strconv"  | 
 | 7 | +	"slices"  | 
10 | 8 | 
 
  | 
 | 9 | +	"github.com/fatih/color"  | 
11 | 10 | 	"github.com/spf13/cobra"  | 
12 | 11 | 	"github.com/spf13/pflag"  | 
13 | 12 | 
 
  | 
14 | 13 | 	"github.com/golangci/golangci-lint/pkg/config"  | 
15 |  | -	"github.com/golangci/golangci-lint/pkg/exitcodes"  | 
16 | 14 | 	"github.com/golangci/golangci-lint/pkg/logutils"  | 
 | 15 | +	"github.com/golangci/golangci-lint/pkg/report"  | 
17 | 16 | )  | 
18 | 17 | 
 
  | 
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{}  | 
24 | 38 | 
 
  | 
25 |  | -func (e *Executor) initRoot() {  | 
26 | 39 | 	rootCmd := &cobra.Command{  | 
27 | 40 | 		Use:   "golangci-lint",  | 
28 | 41 | 		Short: "golangci-lint is a smart linters runner.",  | 
29 | 42 | 		Long:  `Smart, fast linters runner.`,  | 
30 | 43 | 		Args:  cobra.NoArgs,  | 
31 | 44 | 		RunE: func(cmd *cobra.Command, _ []string) error {  | 
 | 45 | +			if c.opts.PrintVersion {  | 
 | 46 | +				_ = printVersion(logutils.StdOut, info)  | 
 | 47 | +				return nil  | 
 | 48 | +			}  | 
 | 49 | + | 
32 | 50 | 			return cmd.Help()  | 
33 | 51 | 		},  | 
34 |  | -		PersistentPreRunE:  e.persistentPreRun,  | 
35 |  | -		PersistentPostRunE: e.persistentPostRun,  | 
36 | 52 | 	}  | 
37 | 53 | 
 
  | 
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"))  | 
42 | 56 | 
 
  | 
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)  | 
48 | 58 | 
 
  | 
49 |  | -	runtime.GOMAXPROCS(e.cfg.Run.Concurrency)  | 
 | 59 | +	reportData := &report.Data{}  | 
 | 60 | +	log := report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), reportData)  | 
50 | 61 | 
 
  | 
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 | +	)  | 
60 | 70 | 
 
  | 
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)  | 
66 | 72 | 
 
  | 
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  | 
76 | 75 | 
 
  | 
77 |  | -	return nil  | 
 | 76 | +	return c  | 
78 | 77 | }  | 
79 | 78 | 
 
  | 
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  | 
83 | 83 | 	}  | 
84 | 84 | 
 
  | 
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 | +}  | 
90 | 87 | 
 
  | 
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 | +}  | 
94 | 92 | 
 
  | 
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  | 
99 | 97 | 	}  | 
100 | 98 | 
 
  | 
101 |  | -	if e.cfg.Run.TracePath != "" {  | 
102 |  | -		trace.Stop()  | 
 | 99 | +	if opts == nil {  | 
 | 100 | +		return nil  | 
103 | 101 | 	}  | 
104 | 102 | 
 
  | 
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 | +	}  | 
106 | 115 | 
 
  | 
107 | 116 | 	return nil  | 
108 | 117 | }  | 
109 | 118 | 
 
  | 
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)  | 
113 | 123 | 
 
  | 
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}  | 
117 | 126 | 
 
  | 
118 |  | -	fs.IntVarP(&cfg.Run.Concurrency, "concurrency", "j", getDefaultConcurrency(),  | 
119 |  | -		wh("Number of CPUs to use (Default: number of logical CPUs)"))  | 
 | 127 | +	opts := &rootOptions{}  | 
120 | 128 | 
 
  | 
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)  | 
123 | 134 | 
 
  | 
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  | 
138 | 136 | 
 
  | 
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 | +		}  | 
142 | 141 | 
 
  | 
143 |  | -	if memBytes < Kb {  | 
144 |  | -		return fmt.Sprintf("%db", memBytes)  | 
 | 142 | +		return nil, fmt.Errorf("can't parse args: %w", err)  | 
145 | 143 | 	}  | 
146 |  | -	if memBytes < Mb {  | 
147 |  | -		return fmt.Sprintf("%dkb", memBytes/Kb)  | 
148 |  | -	}  | 
149 |  | -	return fmt.Sprintf("%dmb", memBytes/Mb)  | 
 | 144 | + | 
 | 145 | +	return opts, nil  | 
150 | 146 | }  | 
151 | 147 | 
 
  | 
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)  | 
157 | 165 | 	}  | 
158 | 166 | 
 
  | 
159 |  | -	return runtime.NumCPU()  | 
 | 167 | +	return cleanArgs  | 
160 | 168 | }  | 
0 commit comments