diff --git a/cmd/golangci-lint/main.go b/cmd/golangci-lint/main.go index 9d1daa81df92..84d979562182 100644 --- a/cmd/golangci-lint/main.go +++ b/cmd/golangci-lint/main.go @@ -36,10 +36,8 @@ func main() { Date: date, } - e := commands.NewExecutor(info) - - if err := e.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "failed executing command with error %v\n", err) + if err := commands.Execute(info); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed executing command with error %v\n", err) os.Exit(exitcodes.Failure) } } diff --git a/docs/src/docs/contributing/architecture.mdx b/docs/src/docs/contributing/architecture.mdx index 3f1aa17da1c9..a449156673a7 100644 --- a/docs/src/docs/contributing/architecture.mdx +++ b/docs/src/docs/contributing/architecture.mdx @@ -22,90 +22,6 @@ graph LR -## Init - -The execution starts here: - -```go title=cmd/golangci-lint/main.go -func main() { - e := commands.NewExecutor(info) - - if err := e.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "failed executing command with error %v\n", err) - os.Exit(exitcodes.Failure) - } -} -``` - -The **executer** is our abstraction: - -```go title=pkg/commands/executor.go -type Executor struct { - rootCmd *cobra.Command - runCmd *cobra.Command - lintersCmd *cobra.Command - - exitCode int - buildInfo BuildInfo - - cfg *config.Config - log logutils.Log - reportData report.Data - DBManager *lintersdb.Manager - EnabledLintersSet *lintersdb.EnabledSet - contextLoader *lint.ContextLoader - goenv *goutil.Env - fileCache *fsutils.FileCache - lineCache *fsutils.LineCache - pkgCache *pkgcache.Cache - debugf logutils.DebugFunc - sw *timeutils.Stopwatch - - loadGuard *load.Guard - flock *flock.Flock -} -``` - -We use dependency injection and all root dependencies are stored in this executor. - -In the function `NewExecutor` we do the following: - -1. Initialize dependencies. -2. Initialize [cobra](https://github.com/spf13/cobra) commands. -3. Parse the config file using [viper](https://github.com/spf13/viper) and merge it with command line arguments. - -The following execution is controlled by `cobra`. If a user executes `golangci-lint run` -then `cobra` executes `e.runCmd`. - -Different `cobra` commands have different runners, e.g. a `run` command is configured in the following way: - -```go title=pkg/commands/run.go -func (e *Executor) initRun() { - e.runCmd = &cobra.Command{ - Use: "run", - Short: "Run the linters", - Run: e.executeRun, - PreRunE: func(_ *cobra.Command, _ []string) error { - if ok := e.acquireFileLock(); !ok { - return errors.New("parallel golangci-lint is running") - } - return nil - }, - PostRun: func(_ *cobra.Command, _ []string) { - e.releaseFileLock() - }, - } - e.rootCmd.AddCommand(e.runCmd) - - e.runCmd.SetOut(logutils.StdOut) // use custom output to properly color it in Windows terminals - e.runCmd.SetErr(logutils.StdErr) - - e.initRunConfiguration(e.runCmd) -} -``` - -The primary execution function of the `run` command is `executeRun`. - ## Load Packages Loading packages is listing all packages and their recursive dependencies for analysis. diff --git a/pkg/commands/cache.go b/pkg/commands/cache.go index 3dbe2427f578..4aa8130518b0 100644 --- a/pkg/commands/cache.go +++ b/pkg/commands/cache.go @@ -1,24 +1,24 @@ package commands import ( - "bytes" - "crypto/sha256" "fmt" - "io" "os" "path/filepath" - "strings" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" "github.com/golangci/golangci-lint/internal/cache" - "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/fsutils" "github.com/golangci/golangci-lint/pkg/logutils" ) -func (e *Executor) initCache() { +type cacheCommand struct { + cmd *cobra.Command +} + +func newCacheCommand() *cacheCommand { + c := &cacheCommand{} + cacheCmd := &cobra.Command{ Use: "cache", Short: "Cache control and information", @@ -28,28 +28,31 @@ func (e *Executor) initCache() { }, } - cacheCmd.AddCommand(&cobra.Command{ - Use: "clean", - Short: "Clean cache", - Args: cobra.NoArgs, - ValidArgsFunction: cobra.NoFileCompletions, - RunE: e.executeCacheClean, - }) - cacheCmd.AddCommand(&cobra.Command{ - Use: "status", - Short: "Show cache status", - Args: cobra.NoArgs, - ValidArgsFunction: cobra.NoFileCompletions, - Run: e.executeCacheStatus, - }) + cacheCmd.AddCommand( + &cobra.Command{ + Use: "clean", + Short: "Clean cache", + Args: cobra.NoArgs, + ValidArgsFunction: cobra.NoFileCompletions, + RunE: c.executeClean, + }, + &cobra.Command{ + Use: "status", + Short: "Show cache status", + Args: cobra.NoArgs, + ValidArgsFunction: cobra.NoFileCompletions, + Run: c.executeStatus, + }, + ) - // TODO: add trim command? + c.cmd = cacheCmd - e.rootCmd.AddCommand(cacheCmd) + return c } -func (e *Executor) executeCacheClean(_ *cobra.Command, _ []string) error { +func (c *cacheCommand) executeClean(_ *cobra.Command, _ []string) error { cacheDir := cache.DefaultDir() + if err := os.RemoveAll(cacheDir); err != nil { return fmt.Errorf("failed to remove dir %s: %w", cacheDir, err) } @@ -57,13 +60,13 @@ func (e *Executor) executeCacheClean(_ *cobra.Command, _ []string) error { return nil } -func (e *Executor) executeCacheStatus(_ *cobra.Command, _ []string) { +func (c *cacheCommand) executeStatus(_ *cobra.Command, _ []string) { cacheDir := cache.DefaultDir() - fmt.Fprintf(logutils.StdOut, "Dir: %s\n", cacheDir) + _, _ = fmt.Fprintf(logutils.StdOut, "Dir: %s\n", cacheDir) cacheSizeBytes, err := dirSizeBytes(cacheDir) if err == nil { - fmt.Fprintf(logutils.StdOut, "Size: %s\n", fsutils.PrettifyBytesCount(cacheSizeBytes)) + _, _ = fmt.Fprintf(logutils.StdOut, "Size: %s\n", fsutils.PrettifyBytesCount(cacheSizeBytes)) } } @@ -77,68 +80,3 @@ func dirSizeBytes(path string) (int64, error) { }) return size, err } - -// --- Related to cache but not used directly by the cache command. - -func initHashSalt(version string, cfg *config.Config) error { - binSalt, err := computeBinarySalt(version) - if err != nil { - return fmt.Errorf("failed to calculate binary salt: %w", err) - } - - configSalt, err := computeConfigSalt(cfg) - if err != nil { - return fmt.Errorf("failed to calculate config salt: %w", err) - } - - b := bytes.NewBuffer(binSalt) - b.Write(configSalt) - cache.SetSalt(b.Bytes()) - return nil -} - -func computeBinarySalt(version string) ([]byte, error) { - if version != "" && version != "(devel)" { - return []byte(version), nil - } - - if logutils.HaveDebugTag(logutils.DebugKeyBinSalt) { - return []byte("debug"), nil - } - - p, err := os.Executable() - if err != nil { - return nil, err - } - f, err := os.Open(p) - if err != nil { - return nil, err - } - defer f.Close() - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return nil, err - } - return h.Sum(nil), nil -} - -// computeConfigSalt computes configuration hash. -// We don't hash all config fields to reduce meaningless cache invalidations. -// At least, it has a huge impact on tests speed. -// Fields: `LintersSettings` and `Run.BuildTags`. -func computeConfigSalt(cfg *config.Config) ([]byte, error) { - lintersSettingsBytes, err := yaml.Marshal(cfg.LintersSettings) - if err != nil { - return nil, fmt.Errorf("failed to json marshal config linter settings: %w", err) - } - - configData := bytes.NewBufferString("linters-settings=") - configData.Write(lintersSettingsBytes) - configData.WriteString("\nbuild-tags=%s" + strings.Join(cfg.Run.BuildTags, ",")) - - h := sha256.New() - if _, err := h.Write(configData.Bytes()); err != nil { - return nil, err - } - return h.Sum(nil), nil -} diff --git a/pkg/commands/config.go b/pkg/commands/config.go index f269d881bd75..3d74c89d33f1 100644 --- a/pkg/commands/config.go +++ b/pkg/commands/config.go @@ -5,15 +5,27 @@ import ( "os" "github.com/spf13/cobra" - "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/exitcodes" "github.com/golangci/golangci-lint/pkg/fsutils" + "github.com/golangci/golangci-lint/pkg/logutils" ) -func (e *Executor) initConfig() { +type configCommand struct { + viper *viper.Viper + cmd *cobra.Command + + log logutils.Log +} + +func newConfigCommand(log logutils.Log) *configCommand { + c := &configCommand{ + viper: viper.New(), + log: log, + } + configCmd := &cobra.Command{ Use: "config", Short: "Config file information", @@ -23,25 +35,38 @@ func (e *Executor) initConfig() { }, } - pathCmd := &cobra.Command{ - Use: "path", - Short: "Print used config path", - Args: cobra.NoArgs, - ValidArgsFunction: cobra.NoFileCompletions, - Run: e.executePath, - } + configCmd.AddCommand( + &cobra.Command{ + Use: "path", + Short: "Print used config path", + Args: cobra.NoArgs, + ValidArgsFunction: cobra.NoFileCompletions, + Run: c.execute, + PreRunE: c.preRunE, + }, + ) - fs := pathCmd.Flags() - fs.SortFlags = false // sort them as they are defined here + c.cmd = configCmd - configCmd.AddCommand(pathCmd) - e.rootCmd.AddCommand(configCmd) + return c } -func (e *Executor) executePath(_ *cobra.Command, _ []string) { - usedConfigFile := e.getUsedConfig() +func (c *configCommand) preRunE(cmd *cobra.Command, _ []string) error { + // The command doesn't depend on the real configuration. + // It only needs to know the path of the configuration file. + loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), config.LoaderOptions{}, config.NewDefault()) + + if err := loader.Load(); err != nil { + return fmt.Errorf("can't load config: %w", err) + } + + return nil +} + +func (c *configCommand) execute(_ *cobra.Command, _ []string) { + usedConfigFile := c.getUsedConfig() if usedConfigFile == "" { - e.log.Warnf("No config file detected") + c.log.Warnf("No config file detected") os.Exit(exitcodes.NoConfigFileDetected) } @@ -50,24 +75,17 @@ func (e *Executor) executePath(_ *cobra.Command, _ []string) { // getUsedConfig returns the resolved path to the golangci config file, // or the empty string if no configuration could be found. -func (e *Executor) getUsedConfig() string { - usedConfigFile := viper.ConfigFileUsed() +func (c *configCommand) getUsedConfig() string { + usedConfigFile := c.viper.ConfigFileUsed() if usedConfigFile == "" { return "" } prettyUsedConfigFile, err := fsutils.ShortestRelPath(usedConfigFile, "") if err != nil { - e.log.Warnf("Can't pretty print config file path: %s", err) + c.log.Warnf("Can't pretty print config file path: %s", err) return usedConfigFile } return prettyUsedConfigFile } - -// --- Related to config but not used directly by the config command. - -func initConfigFileFlagSet(fs *pflag.FlagSet, cfg *config.Run) { - fs.StringVarP(&cfg.Config, "config", "c", "", wh("Read config from file path `PATH`")) - fs.BoolVar(&cfg.NoConfig, "no-config", false, wh("Don't read config file")) -} diff --git a/pkg/commands/executor.go b/pkg/commands/executor.go deleted file mode 100644 index 211466cfb645..000000000000 --- a/pkg/commands/executor.go +++ /dev/null @@ -1,230 +0,0 @@ -package commands - -import ( - "errors" - "fmt" - "os" - "strings" - "time" - - "github.com/fatih/color" - "github.com/gofrs/flock" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - - "github.com/golangci/golangci-lint/internal/pkgcache" - "github.com/golangci/golangci-lint/pkg/config" - "github.com/golangci/golangci-lint/pkg/fsutils" - "github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load" - "github.com/golangci/golangci-lint/pkg/goutil" - "github.com/golangci/golangci-lint/pkg/lint" - "github.com/golangci/golangci-lint/pkg/lint/lintersdb" - "github.com/golangci/golangci-lint/pkg/logutils" - "github.com/golangci/golangci-lint/pkg/report" - "github.com/golangci/golangci-lint/pkg/timeutils" -) - -type Executor struct { - rootCmd *cobra.Command - - runCmd *cobra.Command // used by fixSlicesFlags, printStats - lintersCmd *cobra.Command // used by fixSlicesFlags - - exitCode int - - buildInfo BuildInfo - - cfg *config.Config // cfg is the unmarshaled data from the golangci config file. - - log logutils.Log - debugf logutils.DebugFunc - reportData report.Data - - dbManager *lintersdb.Manager - enabledLintersSet *lintersdb.EnabledSet - - contextLoader *lint.ContextLoader - goenv *goutil.Env - - fileCache *fsutils.FileCache - lineCache *fsutils.LineCache - - flock *flock.Flock -} - -// NewExecutor creates and initializes a new command executor. -func NewExecutor(buildInfo BuildInfo) *Executor { - e := &Executor{ - cfg: config.NewDefault(), - buildInfo: buildInfo, - debugf: logutils.Debug(logutils.DebugKeyExec), - } - - e.log = report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), &e.reportData) - - // init of commands must be done before config file reading because init sets config with the default values of flags. - e.initCommands() - - startedAt := time.Now() - e.debugf("Starting execution...") - - e.initConfiguration() - e.initExecutor() - - e.debugf("Initialized executor in %s", time.Since(startedAt)) - - return e -} - -func (e *Executor) initCommands() { - e.initRoot() - e.initRun() - e.initHelp() - e.initLinters() - e.initConfig() - e.initVersion() - e.initCache() -} - -func (e *Executor) initConfiguration() { - // to set up log level early we need to parse config from command line extra time to find `-v` option. - commandLineCfg, err := getConfigForCommandLine() - if err != nil && !errors.Is(err, pflag.ErrHelp) { - e.log.Fatalf("Can't get config for command line: %s", err) - } - if commandLineCfg != nil { - logutils.SetupVerboseLog(e.log, commandLineCfg.Run.IsVerbose) - - switch commandLineCfg.Output.Color { - case "always": - color.NoColor = false - case "never": - color.NoColor = true - case "auto": - // nothing - default: - e.log.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", commandLineCfg.Output.Color) - } - } - - // init e.cfg by values from config: flags parse will see these values like the default ones. - // It will overwrite them only if the same option is found in command-line: it's ok, command-line has higher priority. - - r := config.NewFileReader(e.cfg, commandLineCfg, e.log.Child(logutils.DebugKeyConfigReader)) - if err = r.Read(); err != nil { - e.log.Fatalf("Can't read config: %s", err) - } - - if commandLineCfg != nil && commandLineCfg.Run.Go != "" { - // This hack allow to have the right Run information at least for the Go version (because the default value of the "go" flag is empty). - // If you put a log for `m.cfg.Run.Go` inside `GetAllSupportedLinterConfigs`, - // you will observe that at end (without this hack) the value will have the right value but too late, - // the linters are already running with the previous uncompleted configuration. - // TODO(ldez) there is a major problem with the executor: - // the parsing of the configuration and the timing to load the configuration and linters are creating unmanageable situations. - // There is no simple solution because it's spaghetti code. - // I need to completely rewrite the command line system and the executor because it's extremely time consuming to debug, - // so it's unmaintainable. - e.cfg.Run.Go = commandLineCfg.Run.Go - } else if e.cfg.Run.Go == "" { - e.cfg.Run.Go = config.DetectGoVersion() - } - - // Slice options must be explicitly set for proper merging of config and command-line options. - fixSlicesFlags(e.runCmd.Flags()) - fixSlicesFlags(e.lintersCmd.Flags()) -} - -func (e *Executor) initExecutor() { - e.dbManager = lintersdb.NewManager(e.cfg, e.log) - - e.enabledLintersSet = lintersdb.NewEnabledSet(e.dbManager, - lintersdb.NewValidator(e.dbManager), e.log.Child(logutils.DebugKeyLintersDB), e.cfg) - - e.goenv = goutil.NewEnv(e.log.Child(logutils.DebugKeyGoEnv)) - - e.fileCache = fsutils.NewFileCache() - e.lineCache = fsutils.NewLineCache(e.fileCache) - - sw := timeutils.NewStopwatch("pkgcache", e.log.Child(logutils.DebugKeyStopwatch)) - - pkgCache, err := pkgcache.NewCache(sw, e.log.Child(logutils.DebugKeyPkgCache)) - if err != nil { - e.log.Fatalf("Failed to build packages cache: %s", err) - } - - e.contextLoader = lint.NewContextLoader(e.cfg, e.log.Child(logutils.DebugKeyLoader), e.goenv, - e.lineCache, e.fileCache, pkgCache, load.NewGuard()) - - if err = initHashSalt(e.buildInfo.Version, e.cfg); err != nil { - e.log.Fatalf("Failed to init hash salt: %s", err) - } -} - -func (e *Executor) Execute() error { - return e.rootCmd.Execute() -} - -func getConfigForCommandLine() (*config.Config, error) { - // We use another pflag.FlagSet here to not set `changed` flag - // on cmd.Flags() options. Otherwise, string slice options will be duplicated. - fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError) - - var cfg config.Config - // Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations: - // `changed` variable inside string slice vars will be shared. - // Use another config variable here, not e.cfg, to not - // affect main parsing by this parsing of only config option. - initRunFlagSet(fs, &cfg) - initVersionFlagSet(fs, &cfg) - - // Parse max options, even force version option: don't want - // to get access to Executor here: it's error-prone to use - // cfg vs e.cfg. - initRootFlagSet(fs, &cfg) - - fs.Usage = func() {} // otherwise, help text will be printed twice - if err := fs.Parse(os.Args); err != nil { - if errors.Is(err, pflag.ErrHelp) { - return nil, err - } - - return nil, fmt.Errorf("can't parse args: %w", err) - } - - return &cfg, nil -} - -func fixSlicesFlags(fs *pflag.FlagSet) { - // It's a dirty hack to set flag.Changed to true for every string slice flag. - // It's necessary to merge config and command-line slices: otherwise command-line - // flags will always overwrite ones from the config. - fs.VisitAll(func(f *pflag.Flag) { - if f.Value.Type() != "stringSlice" { - return - } - - s, err := fs.GetStringSlice(f.Name) - if err != nil { - return - } - - if s == nil { // assume that every string slice flag has nil as the default - return - } - - var safe []string - for _, v := range s { - // add quotes to escape comma because spf13/pflag use a CSV parser: - // https://github.com/spf13/pflag/blob/85dd5c8bc61cfa382fecd072378089d4e856579d/string_slice.go#L43 - safe = append(safe, `"`+v+`"`) - } - - // calling Set sets Changed to true: next Set calls will append, not overwrite - _ = f.Value.Set(strings.Join(safe, ",")) - }) -} - -func wh(text string) string { - return color.GreenString(text) -} diff --git a/pkg/commands/flagsets.go b/pkg/commands/flagsets.go new file mode 100644 index 000000000000..0928b41e3a0f --- /dev/null +++ b/pkg/commands/flagsets.go @@ -0,0 +1,104 @@ +package commands + +import ( + "fmt" + "strings" + + "github.com/fatih/color" + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/golangci/golangci-lint/pkg/commands/internal" + "github.com/golangci/golangci-lint/pkg/config" + "github.com/golangci/golangci-lint/pkg/exitcodes" + "github.com/golangci/golangci-lint/pkg/lint/lintersdb" +) + +func setupLintersFlagSet(v *viper.Viper, fs *pflag.FlagSet) { + fs.StringSliceP("disable", "D", nil, color.GreenString("Disable specific linter")) // Hack see Loader.applyStringSliceHack + internal.AddFlagAndBind(v, fs, fs.Bool, "disable-all", "linters.disable-all", false, color.GreenString("Disable all linters")) + + fs.StringSliceP("enable", "E", nil, color.GreenString("Enable specific linter")) // Hack see Loader.applyStringSliceHack + internal.AddFlagAndBind(v, fs, fs.Bool, "enable-all", "linters.enable-all", false, color.GreenString("Enable all linters")) + + internal.AddFlagAndBind(v, fs, fs.Bool, "fast", "linters.fast", false, + color.GreenString("Enable only fast linters from enabled linters set (first run won't be fast)")) + + // Hack see Loader.applyStringSliceHack + fs.StringSliceP("presets", "p", nil, + color.GreenString(fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint help linters' to see "+ + "them. This option implies option --disable-all", strings.Join(lintersdb.AllPresets(), "|")))) +} + +func setupRunFlagSet(v *viper.Viper, fs *pflag.FlagSet) { + internal.AddFlagAndBindP(v, fs, fs.IntP, "concurrency", "j", "run.concurrency", getDefaultConcurrency(), + color.GreenString("Number of CPUs to use (Default: number of logical CPUs)")) + + internal.AddFlagAndBind(v, fs, fs.String, "modules-download-mode", "run.modules-download-mode", "", + color.GreenString("Modules download mode. If not empty, passed as -mod= to go tools")) + internal.AddFlagAndBind(v, fs, fs.Int, "issues-exit-code", "run.issues-exit-code", exitcodes.IssuesFound, + color.GreenString("Exit code when issues were found")) + internal.AddFlagAndBind(v, fs, fs.String, "go", "run.go", "", color.GreenString("Targeted Go version")) + fs.StringSlice("build-tags", nil, color.GreenString("Build tags")) // Hack see Loader.applyStringSliceHack + + internal.AddFlagAndBind(v, fs, fs.Duration, "timeout", "run.timeout", defaultTimeout, color.GreenString("Timeout for total work")) + + internal.AddFlagAndBind(v, fs, fs.Bool, "tests", "run.tests", true, color.GreenString("Analyze tests (*_test.go)")) + fs.StringSlice("skip-dirs", nil, color.GreenString("Regexps of directories to skip")) // Hack see Loader.applyStringSliceHack + internal.AddFlagAndBind(v, fs, fs.Bool, "skip-dirs-use-default", "run.skip-dirs-use-default", true, getDefaultDirectoryExcludeHelp()) + fs.StringSlice("skip-files", nil, color.GreenString("Regexps of files to skip")) // Hack see Loader.applyStringSliceHack + + const allowParallelDesc = "Allow multiple parallel golangci-lint instances running. " + + "If false (default) - golangci-lint acquires file lock on start." + internal.AddFlagAndBind(v, fs, fs.Bool, "allow-parallel-runners", "run.allow-parallel-runners", false, + color.GreenString(allowParallelDesc)) + const allowSerialDesc = "Allow multiple golangci-lint instances running, but serialize them around a lock. " + + "If false (default) - golangci-lint exits with an error if it fails to acquire file lock on start." + internal.AddFlagAndBind(v, fs, fs.Bool, "allow-serial-runners", "run.allow-serial-runners", false, color.GreenString(allowSerialDesc)) + internal.AddFlagAndBind(v, fs, fs.Bool, "show-stats", "run.show-stats", false, color.GreenString("Show statistics per linter")) +} + +func setupOutputFlagSet(v *viper.Viper, fs *pflag.FlagSet) { + internal.AddFlagAndBind(v, fs, fs.String, "out-format", "output.format", config.OutFormatColoredLineNumber, + color.GreenString(fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|")))) + internal.AddFlagAndBind(v, fs, fs.Bool, "print-issued-lines", "output.print-issued-lines", true, + color.GreenString("Print lines of code with issue")) + internal.AddFlagAndBind(v, fs, fs.Bool, "print-linter-name", "output.print-linter-name", true, + color.GreenString("Print linter name in issue line")) + internal.AddFlagAndBind(v, fs, fs.Bool, "uniq-by-line", "output.uniq-by-line", true, + color.GreenString("Make issues output unique by line")) + internal.AddFlagAndBind(v, fs, fs.Bool, "sort-results", "output.sort-results", false, + color.GreenString("Sort linter results")) + internal.AddFlagAndBind(v, fs, fs.String, "path-prefix", "output.path-prefix", "", + color.GreenString("Path prefix to add to output")) +} + +//nolint:gomnd +func setupIssuesFlagSet(v *viper.Viper, fs *pflag.FlagSet) { + fs.StringSliceP("exclude", "e", nil, color.GreenString("Exclude issue by regexp")) // Hack see Loader.applyStringSliceHack + internal.AddFlagAndBind(v, fs, fs.Bool, "exclude-use-default", "issues.exclude-use-default", true, + getDefaultIssueExcludeHelp()) + internal.AddFlagAndBind(v, fs, fs.Bool, "exclude-case-sensitive", "issues.exclude-case-sensitive", false, + color.GreenString("If set to true exclude and exclude rules regular expressions are case-sensitive")) + + internal.AddFlagAndBind(v, fs, fs.Int, "max-issues-per-linter", "issues.max-issues-per-linter", 50, + color.GreenString("Maximum issues count per one linter. Set to 0 to disable")) + internal.AddFlagAndBind(v, fs, fs.Int, "max-same-issues", "issues.max-same-issues", 3, + color.GreenString("Maximum count of issues with the same text. Set to 0 to disable")) + + const newDesc = "Show only new issues: if there are unstaged changes or untracked files, only those changes " + + "are analyzed, else only changes in HEAD~ are analyzed.\nIt's a super-useful option for integration " + + "of golangci-lint into existing large codebase.\nIt's not practical to fix all existing issues at " + + "the moment of integration: much better to not allow issues in new code.\nFor CI setups, prefer " + + "--new-from-rev=HEAD~, as --new can skip linting the current patch if any scripts generate " + + "unstaged files before golangci-lint runs." + internal.AddFlagAndBindP(v, fs, fs.BoolP, "new", "n", "issues.new", false, color.GreenString(newDesc)) + internal.AddFlagAndBind(v, fs, fs.String, "new-from-rev", "issues.new-from-rev", "", + color.GreenString("Show only new issues created after git revision `REV`")) + internal.AddFlagAndBind(v, fs, fs.String, "new-from-patch", "issues.new-from-patch", "", + color.GreenString("Show only new issues created in git patch with file path `PATH`")) + internal.AddFlagAndBind(v, fs, fs.Bool, "whole-files", "issues.whole-files", false, + color.GreenString("Show issues in any part of update files (requires new-from-rev or new-from-patch)")) + internal.AddFlagAndBind(v, fs, fs.Bool, "fix", "issues.fix", false, + color.GreenString("Fix found issues (if it's supported by the linter)")) +} diff --git a/pkg/commands/help.go b/pkg/commands/help.go index 016df2c13a56..d6f0ce11ce83 100644 --- a/pkg/commands/help.go +++ b/pkg/commands/help.go @@ -8,12 +8,23 @@ import ( "github.com/fatih/color" "github.com/spf13/cobra" + "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/lintersdb" "github.com/golangci/golangci-lint/pkg/logutils" ) -func (e *Executor) initHelp() { +type helpCommand struct { + cmd *cobra.Command + + dbManager *lintersdb.Manager + + log logutils.Log +} + +func newHelpCommand(logger logutils.Log) *helpCommand { + c := &helpCommand{log: logger} + helpCmd := &cobra.Command{ Use: "help", Short: "Help", @@ -23,20 +34,31 @@ func (e *Executor) initHelp() { }, } - helpCmd.AddCommand(&cobra.Command{ - Use: "linters", - Short: "Help about linters", - Args: cobra.NoArgs, - ValidArgsFunction: cobra.NoFileCompletions, - Run: e.executeHelp, - }) + helpCmd.AddCommand( + &cobra.Command{ + Use: "linters", + Short: "Help about linters", + Args: cobra.NoArgs, + ValidArgsFunction: cobra.NoFileCompletions, + Run: c.execute, + PreRun: c.preRun, + }, + ) + + c.cmd = helpCmd - e.rootCmd.SetHelpCommand(helpCmd) + return c } -func (e *Executor) executeHelp(_ *cobra.Command, _ []string) { +func (c *helpCommand) preRun(_ *cobra.Command, _ []string) { + // The command doesn't depend on the real configuration. + // It just needs the list of all plugins and all presets. + c.dbManager = lintersdb.NewManager(config.NewDefault(), c.log) +} + +func (c *helpCommand) execute(_ *cobra.Command, _ []string) { var enabledLCs, disabledLCs []*linter.Config - for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() { + for _, lc := range c.dbManager.GetAllSupportedLinterConfigs() { if lc.Internal { continue } @@ -49,13 +71,19 @@ func (e *Executor) executeHelp(_ *cobra.Command, _ []string) { } color.Green("Enabled by default linters:\n") - printLinterConfigs(enabledLCs) + printLinters(enabledLCs) + color.Red("\nDisabled by default linters:\n") - printLinterConfigs(disabledLCs) + printLinters(disabledLCs) color.Green("\nLinters presets:") + c.printPresets() +} + +func (c *helpCommand) printPresets() { for _, p := range lintersdb.AllPresets() { - linters := e.dbManager.GetAllLinterConfigsForPreset(p) + linters := c.dbManager.GetAllLinterConfigsForPreset(p) + var linterNames []string for _, lc := range linters { if lc.Internal { @@ -65,14 +93,16 @@ func (e *Executor) executeHelp(_ *cobra.Command, _ []string) { linterNames = append(linterNames, lc.Name()) } sort.Strings(linterNames) - fmt.Fprintf(logutils.StdOut, "%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", ")) + + _, _ = fmt.Fprintf(logutils.StdOut, "%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", ")) } } -func printLinterConfigs(lcs []*linter.Config) { +func printLinters(lcs []*linter.Config) { sort.Slice(lcs, func(i, j int) bool { return lcs[i].Name() < lcs[j].Name() }) + for _, lc := range lcs { altNamesStr := "" if len(lc.AlternativeNames) != 0 { @@ -91,7 +121,7 @@ func printLinterConfigs(lcs []*linter.Config) { deprecatedMark = " [" + color.RedString("deprecated") + "]" } - fmt.Fprintf(logutils.StdOut, "%s%s%s: %s [fast: %t, auto-fix: %t]\n", color.YellowString(lc.Name()), - altNamesStr, deprecatedMark, linterDescription, !lc.IsSlowLinter(), lc.CanAutoFix) + _, _ = fmt.Fprintf(logutils.StdOut, "%s%s%s: %s [fast: %t, auto-fix: %t]\n", + color.YellowString(lc.Name()), altNamesStr, deprecatedMark, linterDescription, !lc.IsSlowLinter(), lc.CanAutoFix) } } diff --git a/pkg/commands/internal/vibra.go b/pkg/commands/internal/vibra.go new file mode 100644 index 000000000000..5a5306fdb55e --- /dev/null +++ b/pkg/commands/internal/vibra.go @@ -0,0 +1,32 @@ +package internal + +import ( + "fmt" + + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +type FlagFunc[T any] func(name string, value T, usage string) *T + +type FlagPFunc[T any] func(name, shorthand string, value T, usage string) *T + +// AddFlagAndBind adds a Cobra/pflag flag and binds it with Viper. +func AddFlagAndBind[T any](v *viper.Viper, fs *pflag.FlagSet, pfn FlagFunc[T], name, bind string, value T, usage string) { + pfn(name, value, usage) + + err := v.BindPFlag(bind, fs.Lookup(name)) + if err != nil { + panic(fmt.Sprintf("failed to bind flag %s: %v", name, err)) + } +} + +// AddFlagAndBindP adds a Cobra/pflag flag and binds it with Viper. +func AddFlagAndBindP[T any](v *viper.Viper, fs *pflag.FlagSet, pfn FlagPFunc[T], name, shorthand, bind string, value T, usage string) { + pfn(name, shorthand, value, usage) + + err := v.BindPFlag(bind, fs.Lookup(name)) + if err != nil { + panic(fmt.Sprintf("failed to bind flag %s: %v", name, err)) + } +} diff --git a/pkg/commands/linters.go b/pkg/commands/linters.go index 5ed2353fd546..02e9447d4ef3 100644 --- a/pkg/commands/linters.go +++ b/pkg/commands/linters.go @@ -2,40 +2,78 @@ package commands import ( "fmt" - "strings" "github.com/fatih/color" "github.com/spf13/cobra" - "github.com/spf13/pflag" + "github.com/spf13/viper" "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/lintersdb" + "github.com/golangci/golangci-lint/pkg/logutils" ) -func (e *Executor) initLinters() { +type lintersOptions struct { + config.LoaderOptions +} + +type lintersCommand struct { + viper *viper.Viper + cmd *cobra.Command + + opts lintersOptions + + cfg *config.Config + + log logutils.Log + + dbManager *lintersdb.Manager + enabledLintersSet *lintersdb.EnabledSet +} + +func newLintersCommand(logger logutils.Log, cfg *config.Config) *lintersCommand { + c := &lintersCommand{ + viper: viper.New(), + cfg: cfg, + log: logger, + } + lintersCmd := &cobra.Command{ Use: "linters", Short: "List current linters configuration", Args: cobra.NoArgs, ValidArgsFunction: cobra.NoFileCompletions, - RunE: e.executeLinters, + RunE: c.execute, + PreRunE: c.preRunE, } fs := lintersCmd.Flags() fs.SortFlags = false // sort them as they are defined here - initConfigFileFlagSet(fs, &e.cfg.Run) - initLintersFlagSet(fs, &e.cfg.Linters) + setupConfigFileFlagSet(fs, &c.opts.LoaderOptions) + setupLintersFlagSet(c.viper, fs) - e.rootCmd.AddCommand(lintersCmd) + c.cmd = lintersCmd - e.lintersCmd = lintersCmd + return c } -// executeLinters runs the 'linters' CLI command, which displays the supported linters. -func (e *Executor) executeLinters(_ *cobra.Command, _ []string) error { - enabledLintersMap, err := e.enabledLintersSet.GetEnabledLintersMap() +func (c *lintersCommand) preRunE(cmd *cobra.Command, _ []string) error { + loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), c.opts.LoaderOptions, c.cfg) + + if err := loader.Load(); err != nil { + return fmt.Errorf("can't load config: %w", err) + } + + c.dbManager = lintersdb.NewManager(c.cfg, c.log) + c.enabledLintersSet = lintersdb.NewEnabledSet(c.dbManager, + lintersdb.NewValidator(c.dbManager), c.log.Child(logutils.DebugKeyLintersDB), c.cfg) + + return nil +} + +func (c *lintersCommand) execute(_ *cobra.Command, _ []string) error { + enabledLintersMap, err := c.enabledLintersSet.GetEnabledLintersMap() if err != nil { return fmt.Errorf("can't get enabled linters: %w", err) } @@ -43,7 +81,7 @@ func (e *Executor) executeLinters(_ *cobra.Command, _ []string) error { var enabledLinters []*linter.Config var disabledLCs []*linter.Config - for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() { + for _, lc := range c.dbManager.GetAllSupportedLinterConfigs() { if lc.Internal { continue } @@ -56,20 +94,9 @@ func (e *Executor) executeLinters(_ *cobra.Command, _ []string) error { } color.Green("Enabled by your configuration linters:\n") - printLinterConfigs(enabledLinters) + printLinters(enabledLinters) color.Red("\nDisabled by your configuration linters:\n") - printLinterConfigs(disabledLCs) + printLinters(disabledLCs) return nil } - -func initLintersFlagSet(fs *pflag.FlagSet, cfg *config.Linters) { - fs.StringSliceVarP(&cfg.Disable, "disable", "D", nil, wh("Disable specific linter")) - fs.BoolVar(&cfg.DisableAll, "disable-all", false, wh("Disable all linters")) - fs.StringSliceVarP(&cfg.Enable, "enable", "E", nil, wh("Enable specific linter")) - fs.BoolVar(&cfg.EnableAll, "enable-all", false, wh("Enable all linters")) - fs.BoolVar(&cfg.Fast, "fast", false, wh("Enable only fast linters from enabled linters set (first run won't be fast)")) - fs.StringSliceVarP(&cfg.Presets, "presets", "p", nil, - wh(fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint help linters' to see "+ - "them. This option implies option --disable-all", strings.Join(lintersdb.AllPresets(), "|")))) -} diff --git a/pkg/commands/root.go b/pkg/commands/root.go index 0ae05480fa73..2a5e7cf1768e 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -1,160 +1,168 @@ package commands import ( + "errors" "fmt" "os" - "runtime" - "runtime/pprof" - "runtime/trace" - "strconv" + "slices" + "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/golangci/golangci-lint/pkg/config" - "github.com/golangci/golangci-lint/pkg/exitcodes" "github.com/golangci/golangci-lint/pkg/logutils" + "github.com/golangci/golangci-lint/pkg/report" ) -const ( - // envHelpRun value: "1". - envHelpRun = "HELP_RUN" - envMemProfileRate = "GL_MEM_PROFILE_RATE" -) +func Execute(info BuildInfo) error { + return newRootCommand(info).Execute() +} + +type rootOptions struct { + PrintVersion bool // Flag only. + + Verbose bool // Flag only. + Color string // Flag only. +} + +type rootCommand struct { + cmd *cobra.Command + opts rootOptions + + log logutils.Log +} + +func newRootCommand(info BuildInfo) *rootCommand { + c := &rootCommand{} -func (e *Executor) initRoot() { rootCmd := &cobra.Command{ Use: "golangci-lint", Short: "golangci-lint is a smart linters runner.", Long: `Smart, fast linters runner.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + if c.opts.PrintVersion { + _ = printVersion(logutils.StdOut, info) + return nil + } + return cmd.Help() }, - PersistentPreRunE: e.persistentPreRun, - PersistentPostRunE: e.persistentPostRun, } - initRootFlagSet(rootCmd.PersistentFlags(), e.cfg) - - e.rootCmd = rootCmd -} + fs := rootCmd.Flags() + fs.BoolVar(&c.opts.PrintVersion, "version", false, color.GreenString("Print version")) -func (e *Executor) persistentPreRun(_ *cobra.Command, _ []string) error { - if e.cfg.Run.PrintVersion { - _ = printVersion(logutils.StdOut, e.buildInfo) - os.Exit(exitcodes.Success) // a return nil is not enough to stop the process because we are inside the `preRun`. - } + setupRootPersistentFlags(rootCmd.PersistentFlags(), &c.opts) - runtime.GOMAXPROCS(e.cfg.Run.Concurrency) + reportData := &report.Data{} + log := report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), reportData) - if e.cfg.Run.CPUProfilePath != "" { - f, err := os.Create(e.cfg.Run.CPUProfilePath) - if err != nil { - return fmt.Errorf("can't create file %s: %w", e.cfg.Run.CPUProfilePath, err) - } - if err := pprof.StartCPUProfile(f); err != nil { - return fmt.Errorf("can't start CPU profiling: %w", err) - } - } + // Dedicated configuration for each command to avoid side effects of bindings. + rootCmd.AddCommand( + newLintersCommand(log, config.NewDefault()).cmd, + newRunCommand(log, config.NewDefault(), reportData, info).cmd, + newCacheCommand().cmd, + newConfigCommand(log).cmd, + newVersionCommand(info).cmd, + ) - if e.cfg.Run.MemProfilePath != "" { - if rate := os.Getenv(envMemProfileRate); rate != "" { - runtime.MemProfileRate, _ = strconv.Atoi(rate) - } - } + rootCmd.SetHelpCommand(newHelpCommand(log).cmd) - if e.cfg.Run.TracePath != "" { - f, err := os.Create(e.cfg.Run.TracePath) - if err != nil { - return fmt.Errorf("can't create file %s: %w", e.cfg.Run.TracePath, err) - } - if err = trace.Start(f); err != nil { - return fmt.Errorf("can't start tracing: %w", err) - } - } + c.log = log + c.cmd = rootCmd - return nil + return c } -func (e *Executor) persistentPostRun(_ *cobra.Command, _ []string) error { - if e.cfg.Run.CPUProfilePath != "" { - pprof.StopCPUProfile() +func (c *rootCommand) Execute() error { + err := setupLogger(c.log) + if err != nil { + return err } - if e.cfg.Run.MemProfilePath != "" { - f, err := os.Create(e.cfg.Run.MemProfilePath) - if err != nil { - return fmt.Errorf("can't create file %s: %w", e.cfg.Run.MemProfilePath, err) - } + return c.cmd.Execute() +} - var ms runtime.MemStats - runtime.ReadMemStats(&ms) - printMemStats(&ms, e.log) +func setupRootPersistentFlags(fs *pflag.FlagSet, opts *rootOptions) { + fs.BoolVarP(&opts.Verbose, "verbose", "v", false, color.GreenString("Verbose output")) + fs.StringVar(&opts.Color, "color", "auto", color.GreenString("Use color when printing; can be 'always', 'auto', or 'never'")) +} - if err := pprof.WriteHeapProfile(f); err != nil { - return fmt.Errorf("can't write heap profile: %w", err) - } - _ = f.Close() +func setupLogger(logger logutils.Log) error { + opts, err := forceRootParsePersistentFlags() + if err != nil && !errors.Is(err, pflag.ErrHelp) { + return err } - if e.cfg.Run.TracePath != "" { - trace.Stop() + if opts == nil { + return nil } - os.Exit(e.exitCode) + logutils.SetupVerboseLog(logger, opts.Verbose) + + switch opts.Color { + case "always": + color.NoColor = false + case "never": + color.NoColor = true + case "auto": + // nothing + default: + logger.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", opts.Color) + } return nil } -func initRootFlagSet(fs *pflag.FlagSet, cfg *config.Config) { - fs.BoolVarP(&cfg.Run.IsVerbose, "verbose", "v", false, wh("Verbose output")) - fs.StringVar(&cfg.Output.Color, "color", "auto", wh("Use color when printing; can be 'always', 'auto', or 'never'")) +func forceRootParsePersistentFlags() (*rootOptions, error) { + // We use another pflag.FlagSet here to not set `changed` flag on cmd.Flags() options. + // Otherwise, string slice options will be duplicated. + fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError) - fs.StringVar(&cfg.Run.CPUProfilePath, "cpu-profile-path", "", wh("Path to CPU profile output file")) - fs.StringVar(&cfg.Run.MemProfilePath, "mem-profile-path", "", wh("Path to memory profile output file")) - fs.StringVar(&cfg.Run.TracePath, "trace-path", "", wh("Path to trace output file")) + // Ignore unknown flags because we will parse the command flags later. + fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true} - fs.IntVarP(&cfg.Run.Concurrency, "concurrency", "j", getDefaultConcurrency(), - wh("Number of CPUs to use (Default: number of logical CPUs)")) + opts := &rootOptions{} - fs.BoolVar(&cfg.Run.PrintVersion, "version", false, wh("Print version")) -} + // Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations: + // `changed` variable inside string slice vars will be shared. + // Use another config variable here, + // to not affect main parsing by this parsing of only config option. + setupRootPersistentFlags(fs, opts) -func printMemStats(ms *runtime.MemStats, logger logutils.Log) { - logger.Infof("Mem stats: alloc=%s total_alloc=%s sys=%s "+ - "heap_alloc=%s heap_sys=%s heap_idle=%s heap_released=%s heap_in_use=%s "+ - "stack_in_use=%s stack_sys=%s "+ - "mspan_sys=%s mcache_sys=%s buck_hash_sys=%s gc_sys=%s other_sys=%s "+ - "mallocs_n=%d frees_n=%d heap_objects_n=%d gc_cpu_fraction=%.2f", - formatMemory(ms.Alloc), formatMemory(ms.TotalAlloc), formatMemory(ms.Sys), - formatMemory(ms.HeapAlloc), formatMemory(ms.HeapSys), - formatMemory(ms.HeapIdle), formatMemory(ms.HeapReleased), formatMemory(ms.HeapInuse), - formatMemory(ms.StackInuse), formatMemory(ms.StackSys), - formatMemory(ms.MSpanSys), formatMemory(ms.MCacheSys), formatMemory(ms.BuckHashSys), - formatMemory(ms.GCSys), formatMemory(ms.OtherSys), - ms.Mallocs, ms.Frees, ms.HeapObjects, ms.GCCPUFraction) -} + fs.Usage = func() {} // otherwise, help text will be printed twice -func formatMemory(memBytes uint64) string { - const Kb = 1024 - const Mb = Kb * 1024 + if err := fs.Parse(safeArgs(fs, os.Args)); err != nil { + if errors.Is(err, pflag.ErrHelp) { + return nil, err + } - if memBytes < Kb { - return fmt.Sprintf("%db", memBytes) + return nil, fmt.Errorf("can't parse args: %w", err) } - if memBytes < Mb { - return fmt.Sprintf("%dkb", memBytes/Kb) - } - return fmt.Sprintf("%dmb", memBytes/Mb) + + return opts, nil } -func getDefaultConcurrency() int { - if os.Getenv(envHelpRun) == "1" { - // Make stable concurrency for generating help documentation. - const prettyConcurrency = 8 - return prettyConcurrency +// Shorthands are a problem because pflag, with UnknownFlags, will try to parse all the letters as options. +// A shorthand can aggregate several letters (ex `ps -aux`) +// The function replaces non-supported shorthands by a dumb flag. +func safeArgs(fs *pflag.FlagSet, args []string) []string { + var shorthands []string + fs.VisitAll(func(flag *pflag.Flag) { + shorthands = append(shorthands, flag.Shorthand) + }) + + var cleanArgs []string + for _, arg := range args { + if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' && !slices.Contains(shorthands, string(arg[1])) { + cleanArgs = append(cleanArgs, "--potato") + continue + } + + cleanArgs = append(cleanArgs, arg) } - return runtime.NumCPU() + return cleanArgs } diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 6af4e2863837..a4ba89058714 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -1,7 +1,9 @@ package commands import ( + "bytes" "context" + "crypto/sha256" "errors" "fmt" "io" @@ -9,7 +11,10 @@ import ( "os" "path/filepath" "runtime" + "runtime/pprof" + "runtime/trace" "sort" + "strconv" "strings" "time" @@ -17,16 +22,25 @@ import ( "github.com/gofrs/flock" "github.com/spf13/cobra" "github.com/spf13/pflag" + "github.com/spf13/viper" "golang.org/x/exp/maps" + "gopkg.in/yaml.v3" + "github.com/golangci/golangci-lint/internal/cache" + "github.com/golangci/golangci-lint/internal/pkgcache" "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/exitcodes" + "github.com/golangci/golangci-lint/pkg/fsutils" + "github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load" + "github.com/golangci/golangci-lint/pkg/goutil" "github.com/golangci/golangci-lint/pkg/lint" "github.com/golangci/golangci-lint/pkg/lint/lintersdb" "github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/packages" "github.com/golangci/golangci-lint/pkg/printers" + "github.com/golangci/golangci-lint/pkg/report" "github.com/golangci/golangci-lint/pkg/result" + "github.com/golangci/golangci-lint/pkg/timeutils" ) const defaultFileMode = 0644 @@ -40,20 +54,68 @@ const ( envMemLogEvery = "GL_MEM_LOG_EVERY" ) -func (e *Executor) initRun() { +const ( + // envHelpRun value: "1". + envHelpRun = "HELP_RUN" + envMemProfileRate = "GL_MEM_PROFILE_RATE" +) + +type runOptions struct { + config.LoaderOptions + + CPUProfilePath string // Flag only. + MemProfilePath string // Flag only. + TracePath string // Flag only. + + PrintResourcesUsage bool // Flag only. +} + +type runCommand struct { + viper *viper.Viper + cmd *cobra.Command + + opts runOptions + + cfg *config.Config + + buildInfo BuildInfo + + dbManager *lintersdb.Manager + enabledLintersSet *lintersdb.EnabledSet + + log logutils.Log + debugf logutils.DebugFunc + reportData *report.Data + + contextLoader *lint.ContextLoader + goenv *goutil.Env + + fileCache *fsutils.FileCache + lineCache *fsutils.LineCache + + flock *flock.Flock + + exitCode int +} + +func newRunCommand(logger logutils.Log, cfg *config.Config, reportData *report.Data, info BuildInfo) *runCommand { + c := &runCommand{ + viper: viper.New(), + log: logger, + debugf: logutils.Debug(logutils.DebugKeyExec), + cfg: cfg, + reportData: reportData, + buildInfo: info, + } + runCmd := &cobra.Command{ - Use: "run", - Short: "Run the linters", - Run: e.executeRun, - PreRunE: func(_ *cobra.Command, _ []string) error { - if ok := e.acquireFileLock(); !ok { - return errors.New("parallel golangci-lint is running") - } - return nil - }, - PostRun: func(_ *cobra.Command, _ []string) { - e.releaseFileLock() - }, + Use: "run", + Short: "Run the linters", + Run: c.execute, + PreRunE: c.preRunE, + PostRun: c.postRun, + PersistentPreRunE: c.persistentPreRunE, + PersistentPostRunE: c.persistentPostRunE, } runCmd.SetOut(logutils.StdOut) // use custom output to properly color it in Windows terminals @@ -62,16 +124,90 @@ func (e *Executor) initRun() { fs := runCmd.Flags() fs.SortFlags = false // sort them as they are defined here - initRunFlagSet(fs, e.cfg) + // Only for testing purpose. + // Don't add other flags here. + fs.BoolVar(&cfg.InternalCmdTest, "internal-cmd-test", false, + color.GreenString("Option is used only for testing golangci-lint command, don't use it")) + _ = fs.MarkHidden("internal-cmd-test") + + setupConfigFileFlagSet(fs, &c.opts.LoaderOptions) + + setupLintersFlagSet(c.viper, fs) + setupRunFlagSet(c.viper, fs) + setupOutputFlagSet(c.viper, fs) + setupIssuesFlagSet(c.viper, fs) - e.rootCmd.AddCommand(runCmd) + setupRunPersistentFlags(runCmd.PersistentFlags(), &c.opts) - e.runCmd = runCmd + c.cmd = runCmd + + return c } -// executeRun executes the 'run' CLI command, which runs the linters. -func (e *Executor) executeRun(_ *cobra.Command, args []string) { - needTrackResources := e.cfg.Run.IsVerbose || e.cfg.Run.PrintResourcesUsage +func (c *runCommand) persistentPreRunE(cmd *cobra.Command, _ []string) error { + if err := c.startTracing(); err != nil { + return err + } + + loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), c.opts.LoaderOptions, c.cfg) + + if err := loader.Load(); err != nil { + return fmt.Errorf("can't load config: %w", err) + } + + runtime.GOMAXPROCS(c.cfg.Run.Concurrency) + + return c.startTracing() +} + +func (c *runCommand) persistentPostRunE(_ *cobra.Command, _ []string) error { + if err := c.stopTracing(); err != nil { + return err + } + + os.Exit(c.exitCode) + + return nil +} + +func (c *runCommand) preRunE(_ *cobra.Command, _ []string) error { + c.dbManager = lintersdb.NewManager(c.cfg, c.log) + c.enabledLintersSet = lintersdb.NewEnabledSet(c.dbManager, + lintersdb.NewValidator(c.dbManager), c.log.Child(logutils.DebugKeyLintersDB), c.cfg) + + c.goenv = goutil.NewEnv(c.log.Child(logutils.DebugKeyGoEnv)) + + c.fileCache = fsutils.NewFileCache() + c.lineCache = fsutils.NewLineCache(c.fileCache) + + sw := timeutils.NewStopwatch("pkgcache", c.log.Child(logutils.DebugKeyStopwatch)) + + pkgCache, err := pkgcache.NewCache(sw, c.log.Child(logutils.DebugKeyPkgCache)) + if err != nil { + return fmt.Errorf("failed to build packages cache: %w", err) + } + + c.contextLoader = lint.NewContextLoader(c.cfg, c.log.Child(logutils.DebugKeyLoader), c.goenv, + c.lineCache, c.fileCache, pkgCache, load.NewGuard()) + + if err = initHashSalt(c.buildInfo.Version, c.cfg); err != nil { + return fmt.Errorf("failed to init hash salt: %w", err) + } + + if ok := c.acquireFileLock(); !ok { + return errors.New("parallel golangci-lint is running") + } + + return nil +} + +func (c *runCommand) postRun(_ *cobra.Command, _ []string) { + c.releaseFileLock() +} + +func (c *runCommand) execute(_ *cobra.Command, args []string) { + needTrackResources := logutils.IsVerbose() || c.opts.PrintResourcesUsage + trackResourcesEndCh := make(chan struct{}) defer func() { // XXX: this defer must be before ctx.cancel defer if needTrackResources { // wait until resource tracking finished to print properly @@ -79,96 +215,154 @@ func (e *Executor) executeRun(_ *cobra.Command, args []string) { } }() - ctx, cancel := context.WithTimeout(context.Background(), e.cfg.Run.Timeout) + ctx, cancel := context.WithTimeout(context.Background(), c.cfg.Run.Timeout) defer cancel() if needTrackResources { - go watchResources(ctx, trackResourcesEndCh, e.log, e.debugf) + go watchResources(ctx, trackResourcesEndCh, c.log, c.debugf) } - if err := e.runAndPrint(ctx, args); err != nil { - e.log.Errorf("Running error: %s", err) - if e.exitCode == exitcodes.Success { + if err := c.runAndPrint(ctx, args); err != nil { + c.log.Errorf("Running error: %s", err) + if c.exitCode == exitcodes.Success { var exitErr *exitcodes.ExitError if errors.As(err, &exitErr) { - e.exitCode = exitErr.Code + c.exitCode = exitErr.Code } else { - e.exitCode = exitcodes.Failure + c.exitCode = exitcodes.Failure } } } - e.setupExitCode(ctx) + c.setupExitCode(ctx) +} + +func (c *runCommand) startTracing() error { + if c.opts.CPUProfilePath != "" { + f, err := os.Create(c.opts.CPUProfilePath) + if err != nil { + return fmt.Errorf("can't create file %s: %w", c.opts.CPUProfilePath, err) + } + if err := pprof.StartCPUProfile(f); err != nil { + return fmt.Errorf("can't start CPU profiling: %w", err) + } + } + + if c.opts.MemProfilePath != "" { + if rate := os.Getenv(envMemProfileRate); rate != "" { + runtime.MemProfileRate, _ = strconv.Atoi(rate) + } + } + + if c.opts.TracePath != "" { + f, err := os.Create(c.opts.TracePath) + if err != nil { + return fmt.Errorf("can't create file %s: %w", c.opts.TracePath, err) + } + if err = trace.Start(f); err != nil { + return fmt.Errorf("can't start tracing: %w", err) + } + } + + return nil +} + +func (c *runCommand) stopTracing() error { + if c.opts.CPUProfilePath != "" { + pprof.StopCPUProfile() + } + + if c.opts.MemProfilePath != "" { + f, err := os.Create(c.opts.MemProfilePath) + if err != nil { + return fmt.Errorf("can't create file %s: %w", c.opts.MemProfilePath, err) + } + + var ms runtime.MemStats + runtime.ReadMemStats(&ms) + printMemStats(&ms, c.log) + + if err := pprof.WriteHeapProfile(f); err != nil { + return fmt.Errorf("can't write heap profile: %w", err) + } + _ = f.Close() + } + + if c.opts.TracePath != "" { + trace.Stop() + } + + return nil } -func (e *Executor) runAndPrint(ctx context.Context, args []string) error { - if err := e.goenv.Discover(ctx); err != nil { - e.log.Warnf("Failed to discover go env: %s", err) +func (c *runCommand) runAndPrint(ctx context.Context, args []string) error { + if err := c.goenv.Discover(ctx); err != nil { + c.log.Warnf("Failed to discover go env: %s", err) } if !logutils.HaveDebugTag(logutils.DebugKeyLintersOutput) { // Don't allow linters and loader to print anything log.SetOutput(io.Discard) - savedStdout, savedStderr := e.setOutputToDevNull() + savedStdout, savedStderr := c.setOutputToDevNull() defer func() { os.Stdout, os.Stderr = savedStdout, savedStderr }() } - issues, err := e.runAnalysis(ctx, args) + issues, err := c.runAnalysis(ctx, args) if err != nil { return err // XXX: don't loose type } - formats := strings.Split(e.cfg.Output.Format, ",") + formats := strings.Split(c.cfg.Output.Format, ",") for _, format := range formats { out := strings.SplitN(format, ":", 2) if len(out) < 2 { out = append(out, "") } - err := e.printReports(issues, out[1], out[0]) + err := c.printReports(issues, out[1], out[0]) if err != nil { return err } } - e.printStats(issues) + c.printStats(issues) - e.setExitCodeIfIssuesFound(issues) + c.setExitCodeIfIssuesFound(issues) - e.fileCache.PrintStats(e.log) + c.fileCache.PrintStats(c.log) return nil } // runAnalysis executes the linters that have been enabled in the configuration. -func (e *Executor) runAnalysis(ctx context.Context, args []string) ([]result.Issue, error) { - e.cfg.Run.Args = args +func (c *runCommand) runAnalysis(ctx context.Context, args []string) ([]result.Issue, error) { + c.cfg.Run.Args = args - lintersToRun, err := e.enabledLintersSet.GetOptimizedLinters() + lintersToRun, err := c.enabledLintersSet.GetOptimizedLinters() if err != nil { return nil, err } - enabledLintersMap, err := e.enabledLintersSet.GetEnabledLintersMap() + enabledLintersMap, err := c.enabledLintersSet.GetEnabledLintersMap() if err != nil { return nil, err } - for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() { + for _, lc := range c.dbManager.GetAllSupportedLinterConfigs() { isEnabled := enabledLintersMap[lc.Name()] != nil - e.reportData.AddLinter(lc.Name(), isEnabled, lc.EnabledByDefault) + c.reportData.AddLinter(lc.Name(), isEnabled, lc.EnabledByDefault) } - lintCtx, err := e.contextLoader.Load(ctx, lintersToRun) + lintCtx, err := c.contextLoader.Load(ctx, lintersToRun) if err != nil { return nil, fmt.Errorf("context loading failed: %w", err) } - lintCtx.Log = e.log.Child(logutils.DebugKeyLintersContext) + lintCtx.Log = c.log.Child(logutils.DebugKeyLintersContext) - runner, err := lint.NewRunner(e.cfg, e.log.Child(logutils.DebugKeyRunner), - e.goenv, e.enabledLintersSet, e.lineCache, e.fileCache, e.dbManager, lintCtx.Packages) + runner, err := lint.NewRunner(c.cfg, c.log.Child(logutils.DebugKeyRunner), + c.goenv, c.enabledLintersSet, c.lineCache, c.fileCache, c.dbManager, lintCtx.Packages) if err != nil { return nil, err } @@ -176,11 +370,11 @@ func (e *Executor) runAnalysis(ctx context.Context, args []string) ([]result.Iss return runner.Run(ctx, lintersToRun, lintCtx) } -func (e *Executor) setOutputToDevNull() (savedStdout, savedStderr *os.File) { +func (c *runCommand) setOutputToDevNull() (savedStdout, savedStderr *os.File) { savedStdout, savedStderr = os.Stdout, os.Stderr devNull, err := os.Open(os.DevNull) if err != nil { - e.log.Warnf("Can't open null device %q: %s", os.DevNull, err) + c.log.Warnf("Can't open null device %q: %s", os.DevNull, err) return } @@ -188,19 +382,19 @@ func (e *Executor) setOutputToDevNull() (savedStdout, savedStderr *os.File) { return } -func (e *Executor) setExitCodeIfIssuesFound(issues []result.Issue) { +func (c *runCommand) setExitCodeIfIssuesFound(issues []result.Issue) { if len(issues) != 0 { - e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound + c.exitCode = c.cfg.Run.ExitCodeIfIssuesFound } } -func (e *Executor) printReports(issues []result.Issue, path, format string) error { - w, shouldClose, err := e.createWriter(path) +func (c *runCommand) printReports(issues []result.Issue, path, format string) error { + w, shouldClose, err := c.createWriter(path) if err != nil { return fmt.Errorf("can't create output for %s: %w", path, err) } - p, err := e.createPrinter(format, w) + p, err := c.createPrinter(format, w) if err != nil { if file, ok := w.(io.Closer); shouldClose && ok { _ = file.Close() @@ -222,7 +416,7 @@ func (e *Executor) printReports(issues []result.Issue, path, format string) erro return nil } -func (e *Executor) createWriter(path string) (io.Writer, bool, error) { +func (c *runCommand) createWriter(path string) (io.Writer, bool, error) { if path == "" || path == "stdout" { return logutils.StdOut, false, nil } @@ -236,19 +430,19 @@ func (e *Executor) createWriter(path string) (io.Writer, bool, error) { return f, true, nil } -func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer, error) { +func (c *runCommand) createPrinter(format string, w io.Writer) (printers.Printer, error) { var p printers.Printer switch format { case config.OutFormatJSON: - p = printers.NewJSON(&e.reportData, w) + p = printers.NewJSON(c.reportData, w) case config.OutFormatColoredLineNumber, config.OutFormatLineNumber: - p = printers.NewText(e.cfg.Output.PrintIssuedLine, - format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName, - e.log.Child(logutils.DebugKeyTextPrinter), w) + p = printers.NewText(c.cfg.Output.PrintIssuedLine, + format == config.OutFormatColoredLineNumber, c.cfg.Output.PrintLinterName, + c.log.Child(logutils.DebugKeyTextPrinter), w) case config.OutFormatTab, config.OutFormatColoredTab: - p = printers.NewTab(e.cfg.Output.PrintLinterName, + p = printers.NewTab(c.cfg.Output.PrintLinterName, format == config.OutFormatColoredTab, - e.log.Child(logutils.DebugKeyTabPrinter), w) + c.log.Child(logutils.DebugKeyTabPrinter), w) case config.OutFormatCheckstyle: p = printers.NewCheckstyle(w) case config.OutFormatCodeClimate: @@ -268,13 +462,13 @@ func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer, return p, nil } -func (e *Executor) printStats(issues []result.Issue) { - if !e.cfg.Run.ShowStats { +func (c *runCommand) printStats(issues []result.Issue) { + if !c.cfg.Run.ShowStats { return } if len(issues) == 0 { - e.runCmd.Println("0 issues.") + c.cmd.Println("0 issues.") return } @@ -283,53 +477,53 @@ func (e *Executor) printStats(issues []result.Issue) { stats[issues[idx].FromLinter]++ } - e.runCmd.Printf("%d issues:\n", len(issues)) + c.cmd.Printf("%d issues:\n", len(issues)) keys := maps.Keys(stats) sort.Strings(keys) for _, key := range keys { - e.runCmd.Printf("* %s: %d\n", key, stats[key]) + c.cmd.Printf("* %s: %d\n", key, stats[key]) } } -func (e *Executor) setupExitCode(ctx context.Context) { +func (c *runCommand) setupExitCode(ctx context.Context) { if ctx.Err() != nil { - e.exitCode = exitcodes.Timeout - e.log.Errorf("Timeout exceeded: try increasing it by passing --timeout option") + c.exitCode = exitcodes.Timeout + c.log.Errorf("Timeout exceeded: try increasing it by passing --timeout option") return } - if e.exitCode != exitcodes.Success { + if c.exitCode != exitcodes.Success { return } needFailOnWarnings := os.Getenv(lintersdb.EnvTestRun) == "1" || os.Getenv(envFailOnWarnings) == "1" - if needFailOnWarnings && len(e.reportData.Warnings) != 0 { - e.exitCode = exitcodes.WarningInTest + if needFailOnWarnings && len(c.reportData.Warnings) != 0 { + c.exitCode = exitcodes.WarningInTest return } - if e.reportData.Error != "" { + if c.reportData.Error != "" { // it's a case e.g. when typecheck linter couldn't parse and error and just logged it - e.exitCode = exitcodes.ErrorWasLogged + c.exitCode = exitcodes.ErrorWasLogged return } } -func (e *Executor) acquireFileLock() bool { - if e.cfg.Run.AllowParallelRunners { - e.debugf("Parallel runners are allowed, no locking") +func (c *runCommand) acquireFileLock() bool { + if c.cfg.Run.AllowParallelRunners { + c.debugf("Parallel runners are allowed, no locking") return true } lockFile := filepath.Join(os.TempDir(), "golangci-lint.lock") - e.debugf("Locking on file %s...", lockFile) + c.debugf("Locking on file %s...", lockFile) f := flock.New(lockFile) const retryDelay = time.Second ctx := context.Background() - if !e.cfg.Run.AllowSerialRunners { + if !c.cfg.Run.AllowSerialRunners { const totalTimeout = 5 * time.Second var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, totalTimeout) @@ -339,108 +533,23 @@ func (e *Executor) acquireFileLock() bool { return false } - e.flock = f + c.flock = f return true } -func (e *Executor) releaseFileLock() { - if e.cfg.Run.AllowParallelRunners { +func (c *runCommand) releaseFileLock() { + if c.cfg.Run.AllowParallelRunners { return } - if err := e.flock.Unlock(); err != nil { - e.debugf("Failed to unlock on file: %s", err) + if err := c.flock.Unlock(); err != nil { + c.debugf("Failed to unlock on file: %s", err) } - if err := os.Remove(e.flock.Path()); err != nil { - e.debugf("Failed to remove lock file: %s", err) + if err := os.Remove(c.flock.Path()); err != nil { + c.debugf("Failed to remove lock file: %s", err) } } -//nolint:gomnd -func initRunFlagSet(fs *pflag.FlagSet, cfg *config.Config) { - fs.BoolVar(&cfg.InternalCmdTest, "internal-cmd-test", false, wh("Option is used only for testing golangci-lint command, don't use it")) - if err := fs.MarkHidden("internal-cmd-test"); err != nil { - panic(err) - } - - // --- Output config - - oc := &cfg.Output - fs.StringVar(&oc.Format, "out-format", - config.OutFormatColoredLineNumber, - wh(fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|")))) - fs.BoolVar(&oc.PrintIssuedLine, "print-issued-lines", true, wh("Print lines of code with issue")) - fs.BoolVar(&oc.PrintLinterName, "print-linter-name", true, wh("Print linter name in issue line")) - fs.BoolVar(&oc.UniqByLine, "uniq-by-line", true, wh("Make issues output unique by line")) - fs.BoolVar(&oc.SortResults, "sort-results", false, wh("Sort linter results")) - fs.BoolVar(&oc.PrintWelcomeMessage, "print-welcome", false, wh("Print welcome message")) - fs.StringVar(&oc.PathPrefix, "path-prefix", "", wh("Path prefix to add to output")) - - // --- Run config - - rc := &cfg.Run - - // Config file config - initConfigFileFlagSet(fs, rc) - - fs.StringVar(&rc.ModulesDownloadMode, "modules-download-mode", "", - wh("Modules download mode. If not empty, passed as -mod= to go tools")) - fs.IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code", - exitcodes.IssuesFound, wh("Exit code when issues were found")) - fs.StringVar(&rc.Go, "go", "", wh("Targeted Go version")) - fs.StringSliceVar(&rc.BuildTags, "build-tags", nil, wh("Build tags")) - - fs.DurationVar(&rc.Timeout, "timeout", defaultTimeout, wh("Timeout for total work")) - - fs.BoolVar(&rc.AnalyzeTests, "tests", true, wh("Analyze tests (*_test.go)")) - fs.BoolVar(&rc.PrintResourcesUsage, "print-resources-usage", false, - wh("Print avg and max memory usage of golangci-lint and total time")) - fs.StringSliceVar(&rc.SkipDirs, "skip-dirs", nil, wh("Regexps of directories to skip")) - fs.BoolVar(&rc.UseDefaultSkipDirs, "skip-dirs-use-default", true, getDefaultDirectoryExcludeHelp()) - fs.StringSliceVar(&rc.SkipFiles, "skip-files", nil, wh("Regexps of files to skip")) - - const allowParallelDesc = "Allow multiple parallel golangci-lint instances running. " + - "If false (default) - golangci-lint acquires file lock on start." - fs.BoolVar(&rc.AllowParallelRunners, "allow-parallel-runners", false, wh(allowParallelDesc)) - const allowSerialDesc = "Allow multiple golangci-lint instances running, but serialize them around a lock. " + - "If false (default) - golangci-lint exits with an error if it fails to acquire file lock on start." - fs.BoolVar(&rc.AllowSerialRunners, "allow-serial-runners", false, wh(allowSerialDesc)) - fs.BoolVar(&rc.ShowStats, "show-stats", false, wh("Show statistics per linter")) - - // --- Linters config - - lc := &cfg.Linters - initLintersFlagSet(fs, lc) - - // --- Issues config - - ic := &cfg.Issues - fs.StringSliceVarP(&ic.ExcludePatterns, "exclude", "e", nil, wh("Exclude issue by regexp")) - fs.BoolVar(&ic.UseDefaultExcludes, "exclude-use-default", true, getDefaultIssueExcludeHelp()) - fs.BoolVar(&ic.ExcludeCaseSensitive, "exclude-case-sensitive", false, wh("If set to true exclude "+ - "and exclude rules regular expressions are case sensitive")) - - fs.IntVar(&ic.MaxIssuesPerLinter, "max-issues-per-linter", 50, - wh("Maximum issues count per one linter. Set to 0 to disable")) - fs.IntVar(&ic.MaxSameIssues, "max-same-issues", 3, - wh("Maximum count of issues with the same text. Set to 0 to disable")) - - fs.BoolVarP(&ic.Diff, "new", "n", false, - wh("Show only new issues: if there are unstaged changes or untracked files, only those changes "+ - "are analyzed, else only changes in HEAD~ are analyzed.\nIt's a super-useful option for integration "+ - "of golangci-lint into existing large codebase.\nIt's not practical to fix all existing issues at "+ - "the moment of integration: much better to not allow issues in new code.\nFor CI setups, prefer "+ - "--new-from-rev=HEAD~, as --new can skip linting the current patch if any scripts generate "+ - "unstaged files before golangci-lint runs.")) - fs.StringVar(&ic.DiffFromRevision, "new-from-rev", "", - wh("Show only new issues created after git revision `REV`")) - fs.StringVar(&ic.DiffPatchFilePath, "new-from-patch", "", - wh("Show only new issues created in git patch with file path `PATH`")) - fs.BoolVar(&ic.WholeFiles, "whole-files", false, - wh("Show issues in any part of update files (requires new-from-rev or new-from-patch)")) - fs.BoolVar(&ic.NeedFix, "fix", false, wh("Fix found issues (if it's supported by the linter)")) -} - func watchResources(ctx context.Context, done chan struct{}, logger logutils.Log, debugf logutils.DebugFunc) { startedAt := time.Now() debugf("Started tracking time") @@ -497,6 +606,11 @@ func watchResources(ctx context.Context, done chan struct{}, logger logutils.Log close(done) } +func setupConfigFileFlagSet(fs *pflag.FlagSet, cfg *config.LoaderOptions) { + fs.StringVarP(&cfg.Config, "config", "c", "", color.GreenString("Read config from file path `PATH`")) + fs.BoolVar(&cfg.NoConfig, "no-config", false, color.GreenString("Don't read config file")) +} + func getDefaultIssueExcludeHelp() string { parts := []string{color.GreenString("Use or not use default excludes:")} for _, ep := range config.DefaultExcludePatterns { @@ -517,3 +631,115 @@ func getDefaultDirectoryExcludeHelp() string { parts = append(parts, "") return strings.Join(parts, "\n") } + +func setupRunPersistentFlags(fs *pflag.FlagSet, opts *runOptions) { + fs.BoolVar(&opts.PrintResourcesUsage, "print-resources-usage", false, + color.GreenString("Print avg and max memory usage of golangci-lint and total time")) + + fs.StringVar(&opts.CPUProfilePath, "cpu-profile-path", "", color.GreenString("Path to CPU profile output file")) + fs.StringVar(&opts.MemProfilePath, "mem-profile-path", "", color.GreenString("Path to memory profile output file")) + fs.StringVar(&opts.TracePath, "trace-path", "", color.GreenString("Path to trace output file")) +} + +func getDefaultConcurrency() int { + if os.Getenv(envHelpRun) == "1" { + // Make stable concurrency for generating help documentation. + const prettyConcurrency = 8 + return prettyConcurrency + } + + return runtime.NumCPU() +} + +func printMemStats(ms *runtime.MemStats, logger logutils.Log) { + logger.Infof("Mem stats: alloc=%s total_alloc=%s sys=%s "+ + "heap_alloc=%s heap_sys=%s heap_idle=%s heap_released=%s heap_in_use=%s "+ + "stack_in_use=%s stack_sys=%s "+ + "mspan_sys=%s mcache_sys=%s buck_hash_sys=%s gc_sys=%s other_sys=%s "+ + "mallocs_n=%d frees_n=%d heap_objects_n=%d gc_cpu_fraction=%.2f", + formatMemory(ms.Alloc), formatMemory(ms.TotalAlloc), formatMemory(ms.Sys), + formatMemory(ms.HeapAlloc), formatMemory(ms.HeapSys), + formatMemory(ms.HeapIdle), formatMemory(ms.HeapReleased), formatMemory(ms.HeapInuse), + formatMemory(ms.StackInuse), formatMemory(ms.StackSys), + formatMemory(ms.MSpanSys), formatMemory(ms.MCacheSys), formatMemory(ms.BuckHashSys), + formatMemory(ms.GCSys), formatMemory(ms.OtherSys), + ms.Mallocs, ms.Frees, ms.HeapObjects, ms.GCCPUFraction) +} + +func formatMemory(memBytes uint64) string { + const Kb = 1024 + const Mb = Kb * 1024 + + if memBytes < Kb { + return fmt.Sprintf("%db", memBytes) + } + if memBytes < Mb { + return fmt.Sprintf("%dkb", memBytes/Kb) + } + return fmt.Sprintf("%dmb", memBytes/Mb) +} + +// --- Related to cache. + +func initHashSalt(version string, cfg *config.Config) error { + binSalt, err := computeBinarySalt(version) + if err != nil { + return fmt.Errorf("failed to calculate binary salt: %w", err) + } + + configSalt, err := computeConfigSalt(cfg) + if err != nil { + return fmt.Errorf("failed to calculate config salt: %w", err) + } + + b := bytes.NewBuffer(binSalt) + b.Write(configSalt) + cache.SetSalt(b.Bytes()) + return nil +} + +func computeBinarySalt(version string) ([]byte, error) { + if version != "" && version != "(devel)" { + return []byte(version), nil + } + + if logutils.HaveDebugTag(logutils.DebugKeyBinSalt) { + return []byte("debug"), nil + } + + p, err := os.Executable() + if err != nil { + return nil, err + } + f, err := os.Open(p) + if err != nil { + return nil, err + } + defer f.Close() + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return nil, err + } + return h.Sum(nil), nil +} + +// computeConfigSalt computes configuration hash. +// We don't hash all config fields to reduce meaningless cache invalidations. +// At least, it has a huge impact on tests speed. +// Fields: `LintersSettings` and `Run.BuildTags`. +func computeConfigSalt(cfg *config.Config) ([]byte, error) { + lintersSettingsBytes, err := yaml.Marshal(cfg.LintersSettings) + if err != nil { + return nil, fmt.Errorf("failed to json marshal config linter settings: %w", err) + } + + configData := bytes.NewBufferString("linters-settings=") + configData.Write(lintersSettingsBytes) + configData.WriteString("\nbuild-tags=%s" + strings.Join(cfg.Run.BuildTags, ",")) + + h := sha256.New() + if _, err := h.Write(configData.Bytes()); err != nil { + return nil, err + } + return h.Sum(nil), nil +} diff --git a/pkg/commands/version.go b/pkg/commands/version.go index 18e5716b7e90..124ad7f1a339 100644 --- a/pkg/commands/version.go +++ b/pkg/commands/version.go @@ -8,10 +8,8 @@ import ( "runtime/debug" "strings" + "github.com/fatih/color" "github.com/spf13/cobra" - "github.com/spf13/pflag" - - "github.com/golangci/golangci-lint/pkg/config" ) type BuildInfo struct { @@ -26,65 +24,75 @@ type versionInfo struct { BuildInfo *debug.BuildInfo } -func (e *Executor) initVersion() { +type versionOptions struct { + Format string `mapstructure:"format"` + Debug bool `mapstructure:"debug"` +} + +type versionCommand struct { + cmd *cobra.Command + opts versionOptions + + info BuildInfo +} + +func newVersionCommand(info BuildInfo) *versionCommand { + c := &versionCommand{info: info} + versionCmd := &cobra.Command{ Use: "version", Short: "Version", Args: cobra.NoArgs, ValidArgsFunction: cobra.NoFileCompletions, - RunE: e.executeVersion, + RunE: c.execute, } fs := versionCmd.Flags() fs.SortFlags = false // sort them as they are defined here - initVersionFlagSet(fs, e.cfg) + fs.StringVar(&c.opts.Format, "format", "", color.GreenString("The version's format can be: 'short', 'json'")) + fs.BoolVar(&c.opts.Debug, "debug", false, color.GreenString("Add build information")) + + c.cmd = versionCmd - e.rootCmd.AddCommand(versionCmd) + return c } -func (e *Executor) executeVersion(_ *cobra.Command, _ []string) error { - if e.cfg.Version.Debug { +func (c *versionCommand) execute(_ *cobra.Command, _ []string) error { + if c.opts.Debug { info, ok := debug.ReadBuildInfo() if !ok { return nil } - switch strings.ToLower(e.cfg.Version.Format) { + switch strings.ToLower(c.opts.Format) { case "json": return json.NewEncoder(os.Stdout).Encode(versionInfo{ - Info: e.buildInfo, + Info: c.info, BuildInfo: info, }) default: fmt.Println(info.String()) - return printVersion(os.Stdout, e.buildInfo) + return printVersion(os.Stdout, c.info) } } - switch strings.ToLower(e.cfg.Version.Format) { + switch strings.ToLower(c.opts.Format) { case "short": - fmt.Println(e.buildInfo.Version) + fmt.Println(c.info.Version) return nil case "json": - return json.NewEncoder(os.Stdout).Encode(e.buildInfo) + return json.NewEncoder(os.Stdout).Encode(c.info) default: - return printVersion(os.Stdout, e.buildInfo) + return printVersion(os.Stdout, c.info) } } -func initVersionFlagSet(fs *pflag.FlagSet, cfg *config.Config) { - // Version config - vc := &cfg.Version - fs.StringVar(&vc.Format, "format", "", wh("The version's format can be: 'short', 'json'")) - fs.BoolVar(&vc.Debug, "debug", false, wh("Add build information")) -} - -func printVersion(w io.Writer, buildInfo BuildInfo) error { +func printVersion(w io.Writer, info BuildInfo) error { _, err := fmt.Fprintf(w, "golangci-lint has version %s built with %s from %s on %s\n", - buildInfo.Version, buildInfo.GoVersion, buildInfo.Commit, buildInfo.Date) + info.Version, info.GoVersion, info.Commit, info.Date) return err } diff --git a/pkg/config/config.go b/pkg/config/config.go index f73563a2a4ef..9a4cc97f03f8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,6 +1,8 @@ package config import ( + "errors" + "fmt" "os" "strings" @@ -21,8 +23,6 @@ type Config struct { Issues Issues `mapstructure:"issues"` Severity Severity `mapstructure:"severity"` - Version Version // Flag only. // TODO(ldez) only used by the version command. - InternalCmdTest bool // Option is used only for testing golangci-lint command, don't use it InternalTest bool // Option is used only for testing golangci-lint code, don't use it } @@ -32,6 +32,25 @@ func (c *Config) GetConfigDir() string { return c.cfgDir } +func (c *Config) Validate() error { + for i, rule := range c.Issues.ExcludeRules { + if err := rule.Validate(); err != nil { + return fmt.Errorf("error in exclude rule #%d: %w", i, err) + } + } + + if len(c.Severity.Rules) > 0 && c.Severity.Default == "" { + return errors.New("can't set severity rule option: no default severity defined") + } + for i, rule := range c.Severity.Rules { + if err := rule.Validate(); err != nil { + return fmt.Errorf("error in severity rule #%d: %w", i, err) + } + } + + return nil +} + func NewDefault() *Config { return &Config{ LintersSettings: defaultLintersSettings, @@ -57,7 +76,7 @@ func IsGoGreaterThanOrEqual(current, limit string) bool { return v1.GreaterThanOrEqual(l) } -func DetectGoVersion() string { +func detectGoVersion() string { file, _ := gomoddirectives.GetModuleFile() if file != nil && file.Go != nil && file.Go.Version != "" { diff --git a/pkg/config/loader.go b/pkg/config/loader.go new file mode 100644 index 000000000000..789796d8f6ea --- /dev/null +++ b/pkg/config/loader.go @@ -0,0 +1,287 @@ +package config + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/go-viper/mapstructure/v2" + "github.com/mitchellh/go-homedir" + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/golangci/golangci-lint/pkg/exitcodes" + "github.com/golangci/golangci-lint/pkg/fsutils" + "github.com/golangci/golangci-lint/pkg/logutils" +) + +var errConfigDisabled = errors.New("config is disabled by --no-config") + +type LoaderOptions struct { + Config string // Flag only. The path to the golangci config file, as specified with the --config argument. + NoConfig bool // Flag only. +} + +type Loader struct { + opts LoaderOptions + + viper *viper.Viper + fs *pflag.FlagSet + + log logutils.Log + + cfg *Config +} + +func NewLoader(log logutils.Log, v *viper.Viper, fs *pflag.FlagSet, opts LoaderOptions, cfg *Config) *Loader { + return &Loader{ + opts: opts, + viper: v, + fs: fs, + log: log, + cfg: cfg, + } +} + +func (l *Loader) Load() error { + err := l.setConfigFile() + if err != nil { + return err + } + + err = l.parseConfig() + if err != nil { + return err + } + + l.applyStringSliceHack() + + if l.cfg.Run.Go == "" { + l.cfg.Run.Go = detectGoVersion() + } + + return nil +} + +func (l *Loader) setConfigFile() error { + configFile, err := l.evaluateOptions() + if err != nil { + if errors.Is(err, errConfigDisabled) { + return nil + } + + return fmt.Errorf("can't parse --config option: %w", err) + } + + if configFile != "" { + l.viper.SetConfigFile(configFile) + + // Assume YAML if the file has no extension. + if filepath.Ext(configFile) == "" { + l.viper.SetConfigType("yaml") + } + } else { + l.setupConfigFileSearch() + } + + return nil +} + +func (l *Loader) evaluateOptions() (string, error) { + if l.opts.NoConfig && l.opts.Config != "" { + return "", errors.New("can't combine option --config and --no-config") + } + + if l.opts.NoConfig { + return "", errConfigDisabled + } + + configFile, err := homedir.Expand(l.opts.Config) + if err != nil { + return "", errors.New("failed to expand configuration path") + } + + return configFile, nil +} + +func (l *Loader) setupConfigFileSearch() { + firstArg := extractFirstPathArg() + + absStartPath, err := filepath.Abs(firstArg) + if err != nil { + l.log.Warnf("Can't make abs path for %q: %s", firstArg, err) + absStartPath = filepath.Clean(firstArg) + } + + // start from it + var curDir string + if fsutils.IsDir(absStartPath) { + curDir = absStartPath + } else { + curDir = filepath.Dir(absStartPath) + } + + // find all dirs from it up to the root + configSearchPaths := []string{"./"} + + for { + configSearchPaths = append(configSearchPaths, curDir) + + newCurDir := filepath.Dir(curDir) + if curDir == newCurDir || newCurDir == "" { + break + } + + curDir = newCurDir + } + + // find home directory for global config + if home, err := homedir.Dir(); err != nil { + l.log.Warnf("Can't get user's home directory: %s", err.Error()) + } else if !slices.Contains(configSearchPaths, home) { + configSearchPaths = append(configSearchPaths, home) + } + + l.log.Infof("Config search paths: %s", configSearchPaths) + + l.viper.SetConfigName(".golangci") + + for _, p := range configSearchPaths { + l.viper.AddConfigPath(p) + } +} + +func (l *Loader) parseConfig() error { + if err := l.viper.ReadInConfig(); err != nil { + var configFileNotFoundError viper.ConfigFileNotFoundError + if errors.As(err, &configFileNotFoundError) { + // Load configuration from flags only. + err = l.viper.Unmarshal(l.cfg) + if err != nil { + return err + } + + if err = l.cfg.Validate(); err != nil { + return fmt.Errorf("can't validate config: %w", err) + } + + return nil + } + + return fmt.Errorf("can't read viper config: %w", err) + } + + err := l.setConfigDir() + if err != nil { + return err + } + + // Load configuration from all sources (flags, file). + if err := l.viper.Unmarshal(l.cfg, fileDecoderHook()); err != nil { + return fmt.Errorf("can't unmarshal config by viper: %w", err) + } + + if err := l.cfg.Validate(); err != nil { + return fmt.Errorf("can't validate config: %w", err) + } + + if l.cfg.InternalTest { // just for testing purposes: to detect config file usage + _, _ = fmt.Fprintln(logutils.StdOut, "test") + os.Exit(exitcodes.Success) + } + + return nil +} + +func (l *Loader) setConfigDir() error { + usedConfigFile := l.viper.ConfigFileUsed() + if usedConfigFile == "" { + return nil + } + + if usedConfigFile == os.Stdin.Name() { + usedConfigFile = "" + l.log.Infof("Reading config file stdin") + } else { + var err error + usedConfigFile, err = fsutils.ShortestRelPath(usedConfigFile, "") + if err != nil { + l.log.Warnf("Can't pretty print config file path: %v", err) + } + + l.log.Infof("Used config file %s", usedConfigFile) + } + + usedConfigDir, err := filepath.Abs(filepath.Dir(usedConfigFile)) + if err != nil { + return errors.New("can't get config directory") + } + + l.cfg.cfgDir = usedConfigDir + + return nil +} + +// Hack to append values from StringSlice flags. +// Viper always overrides StringSlice values. +// https://github.com/spf13/viper/issues/1448 +// So StringSlice flags are not bind to Viper like that their values are obtain via Cobra Flags. +func (l *Loader) applyStringSliceHack() { + if l.fs == nil { + return + } + + l.appendStringSlice("enable", &l.cfg.Linters.Enable) + l.appendStringSlice("disable", &l.cfg.Linters.Disable) + l.appendStringSlice("presets", &l.cfg.Linters.Presets) + l.appendStringSlice("build-tags", &l.cfg.Run.BuildTags) + l.appendStringSlice("skip-dirs", &l.cfg.Run.SkipDirs) + l.appendStringSlice("skip-files", &l.cfg.Run.SkipFiles) + l.appendStringSlice("exclude", &l.cfg.Issues.ExcludePatterns) +} + +func (l *Loader) appendStringSlice(name string, current *[]string) { + if l.fs.Changed(name) { + val, _ := l.fs.GetStringSlice(name) + *current = append(*current, val...) + } +} + +func fileDecoderHook() viper.DecoderConfigOption { + return viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( + // Default hooks (https://github.com/spf13/viper/blob/518241257478c557633ab36e474dfcaeb9a3c623/viper.go#L135-L138). + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + + // Needed for forbidigo. + mapstructure.TextUnmarshallerHookFunc(), + )) +} + +func extractFirstPathArg() string { + args := os.Args + + // skip all args ([golangci-lint, run/linters]) before files/dirs list + for len(args) != 0 { + if args[0] == "run" { + args = args[1:] + break + } + + args = args[1:] + } + + // find first file/dir arg + firstArg := "./..." + for _, arg := range args { + if !strings.HasPrefix(arg, "-") { + firstArg = arg + break + } + } + + return firstArg +} diff --git a/pkg/config/output.go b/pkg/config/output.go index 28e4f29b3414..95af38885a67 100644 --- a/pkg/config/output.go +++ b/pkg/config/output.go @@ -28,14 +28,10 @@ var OutFormats = []string{ } type Output struct { - Format string `mapstructure:"format"` - PrintIssuedLine bool `mapstructure:"print-issued-lines"` - PrintLinterName bool `mapstructure:"print-linter-name"` - UniqByLine bool `mapstructure:"uniq-by-line"` - SortResults bool `mapstructure:"sort-results"` - PrintWelcomeMessage bool `mapstructure:"print-welcome"` - PathPrefix string `mapstructure:"path-prefix"` - - // only work with CLI flags because the setup of logs is done before the config file parsing. - Color string // Flag only. + Format string `mapstructure:"format"` + PrintIssuedLine bool `mapstructure:"print-issued-lines"` + PrintLinterName bool `mapstructure:"print-linter-name"` + UniqByLine bool `mapstructure:"uniq-by-line"` + SortResults bool `mapstructure:"sort-results"` + PathPrefix string `mapstructure:"path-prefix"` } diff --git a/pkg/config/reader.go b/pkg/config/reader.go deleted file mode 100644 index 40ff6f0e1c68..000000000000 --- a/pkg/config/reader.go +++ /dev/null @@ -1,248 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "slices" - "strings" - - "github.com/go-viper/mapstructure/v2" - "github.com/mitchellh/go-homedir" - "github.com/spf13/viper" - - "github.com/golangci/golangci-lint/pkg/exitcodes" - "github.com/golangci/golangci-lint/pkg/fsutils" - "github.com/golangci/golangci-lint/pkg/logutils" -) - -type FileReader struct { - log logutils.Log - cfg *Config - commandLineCfg *Config -} - -func NewFileReader(toCfg, commandLineCfg *Config, log logutils.Log) *FileReader { - return &FileReader{ - log: log, - cfg: toCfg, - commandLineCfg: commandLineCfg, - } -} - -func (r *FileReader) Read() error { - // XXX: hack with double parsing for 2 purposes: - // 1. to access "config" option here. - // 2. to give config less priority than command line. - - configFile, err := r.parseConfigOption() - if err != nil { - if errors.Is(err, errConfigDisabled) { - return nil - } - - return fmt.Errorf("can't parse --config option: %w", err) - } - - if configFile != "" { - viper.SetConfigFile(configFile) - - // Assume YAML if the file has no extension. - if filepath.Ext(configFile) == "" { - viper.SetConfigType("yaml") - } - } else { - r.setupConfigFileSearch() - } - - return r.parseConfig() -} - -func (r *FileReader) parseConfig() error { - if err := viper.ReadInConfig(); err != nil { - var configFileNotFoundError viper.ConfigFileNotFoundError - if errors.As(err, &configFileNotFoundError) { - return nil - } - - return fmt.Errorf("can't read viper config: %w", err) - } - - usedConfigFile := viper.ConfigFileUsed() - if usedConfigFile == "" { - return nil - } - - if usedConfigFile == os.Stdin.Name() { - usedConfigFile = "" - r.log.Infof("Reading config file stdin") - } else { - var err error - usedConfigFile, err = fsutils.ShortestRelPath(usedConfigFile, "") - if err != nil { - r.log.Warnf("Can't pretty print config file path: %v", err) - } - - r.log.Infof("Used config file %s", usedConfigFile) - } - - usedConfigDir, err := filepath.Abs(filepath.Dir(usedConfigFile)) - if err != nil { - return errors.New("can't get config directory") - } - r.cfg.cfgDir = usedConfigDir - - if err := viper.Unmarshal(r.cfg, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( - // Default hooks (https://github.com/spf13/viper/blob/518241257478c557633ab36e474dfcaeb9a3c623/viper.go#L135-L138). - mapstructure.StringToTimeDurationHookFunc(), - mapstructure.StringToSliceHookFunc(","), - - // Needed for forbidigo. - mapstructure.TextUnmarshallerHookFunc(), - ))); err != nil { - return fmt.Errorf("can't unmarshal config by viper: %w", err) - } - - if err := r.validateConfig(); err != nil { - return fmt.Errorf("can't validate config: %w", err) - } - - if r.cfg.InternalTest { // just for testing purposes: to detect config file usage - fmt.Fprintln(logutils.StdOut, "test") - os.Exit(exitcodes.Success) - } - - return nil -} - -func (r *FileReader) validateConfig() error { - if len(r.cfg.Run.Args) != 0 { - return errors.New("option run.args in config isn't supported now") - } - - if r.cfg.Run.CPUProfilePath != "" { - return errors.New("option run.cpuprofilepath in config isn't allowed") - } - - if r.cfg.Run.MemProfilePath != "" { - return errors.New("option run.memprofilepath in config isn't allowed") - } - - if r.cfg.Run.TracePath != "" { - return errors.New("option run.tracepath in config isn't allowed") - } - - if r.cfg.Run.IsVerbose { - return errors.New("can't set run.verbose option with config: only on command-line") - } - - for i, rule := range r.cfg.Issues.ExcludeRules { - if err := rule.Validate(); err != nil { - return fmt.Errorf("error in exclude rule #%d: %w", i, err) - } - } - - if len(r.cfg.Severity.Rules) > 0 && r.cfg.Severity.Default == "" { - return errors.New("can't set severity rule option: no default severity defined") - } - for i, rule := range r.cfg.Severity.Rules { - if err := rule.Validate(); err != nil { - return fmt.Errorf("error in severity rule #%d: %w", i, err) - } - } - - return nil -} - -func getFirstPathArg() string { - args := os.Args - - // skip all args ([golangci-lint, run/linters]) before files/dirs list - for len(args) != 0 { - if args[0] == "run" { - args = args[1:] - break - } - - args = args[1:] - } - - // find first file/dir arg - firstArg := "./..." - for _, arg := range args { - if !strings.HasPrefix(arg, "-") { - firstArg = arg - break - } - } - - return firstArg -} - -func (r *FileReader) setupConfigFileSearch() { - firstArg := getFirstPathArg() - absStartPath, err := filepath.Abs(firstArg) - if err != nil { - r.log.Warnf("Can't make abs path for %q: %s", firstArg, err) - absStartPath = filepath.Clean(firstArg) - } - - // start from it - var curDir string - if fsutils.IsDir(absStartPath) { - curDir = absStartPath - } else { - curDir = filepath.Dir(absStartPath) - } - - // find all dirs from it up to the root - configSearchPaths := []string{"./"} - - for { - configSearchPaths = append(configSearchPaths, curDir) - newCurDir := filepath.Dir(curDir) - if curDir == newCurDir || newCurDir == "" { - break - } - curDir = newCurDir - } - - // find home directory for global config - if home, err := homedir.Dir(); err != nil { - r.log.Warnf("Can't get user's home directory: %s", err.Error()) - } else if !slices.Contains(configSearchPaths, home) { - configSearchPaths = append(configSearchPaths, home) - } - - r.log.Infof("Config search paths: %s", configSearchPaths) - viper.SetConfigName(".golangci") - for _, p := range configSearchPaths { - viper.AddConfigPath(p) - } -} - -var errConfigDisabled = errors.New("config is disabled by --no-config") - -func (r *FileReader) parseConfigOption() (string, error) { - cfg := r.commandLineCfg - if cfg == nil { - return "", nil - } - - configFile := cfg.Run.Config - if cfg.Run.NoConfig && configFile != "" { - return "", errors.New("can't combine option --config and --no-config") - } - - if cfg.Run.NoConfig { - return "", errConfigDisabled - } - - configFile, err := homedir.Expand(configFile) - if err != nil { - return "", errors.New("failed to expand configuration path") - } - - return configFile, nil -} diff --git a/pkg/config/run.go b/pkg/config/run.go index 52c6ce262b57..bd81e0d8c21b 100644 --- a/pkg/config/run.go +++ b/pkg/config/run.go @@ -25,20 +25,6 @@ type Run struct { ShowStats bool `mapstructure:"show-stats"` - // --- Flags only section. - - IsVerbose bool `mapstructure:"verbose"` // Flag only - - PrintVersion bool // Flag only. (used by the root command) - - CPUProfilePath string // Flag only. - MemProfilePath string // Flag only. - TracePath string // Flag only. - - PrintResourcesUsage bool `mapstructure:"print-resources-usage"` // Flag only. // TODO(ldez) need to be enforced. - - Config string // Flag only. The path to the golangci config file, as specified with the --config argument. - NoConfig bool // Flag only. - - Args []string // Flag only. // TODO(ldez) identify the real need and usage. + // It's obtain by flags and use for the tests and the context loader. + Args []string // Internal needs. } diff --git a/pkg/lint/lintersdb/custom_linters.go b/pkg/lint/lintersdb/custom_linters.go index 188c14d9138d..76e8fc58922b 100644 --- a/pkg/lint/lintersdb/custom_linters.go +++ b/pkg/lint/lintersdb/custom_linters.go @@ -6,7 +6,6 @@ import ( "path/filepath" "plugin" - "github.com/spf13/viper" "golang.org/x/tools/go/analysis" "github.com/golangci/golangci-lint/pkg/config" @@ -67,12 +66,7 @@ func (m *Manager) loadCustomLinterConfig(name string, settings config.CustomLint func (m *Manager) getAnalyzerPlugin(path string, settings any) ([]*analysis.Analyzer, error) { if !filepath.IsAbs(path) { // resolve non-absolute paths relative to config file's directory - configFilePath := viper.ConfigFileUsed() - absConfigFilePath, err := filepath.Abs(configFilePath) - if err != nil { - return nil, fmt.Errorf("could not get absolute representation of config file path %q: %w", configFilePath, err) - } - path = filepath.Join(filepath.Dir(absConfigFilePath), path) + path = filepath.Join(m.cfg.GetConfigDir(), path) } plug, err := plugin.Open(path) diff --git a/pkg/logutils/logutils.go b/pkg/logutils/logutils.go index 94479bc7b9a8..2b7ac6e287e6 100644 --- a/pkg/logutils/logutils.go +++ b/pkg/logutils/logutils.go @@ -99,8 +99,15 @@ func HaveDebugTag(tag string) bool { return enabledDebugs[tag] } +var verbose bool + func SetupVerboseLog(log Log, isVerbose bool) { if isVerbose { + verbose = isVerbose log.SetLevel(LogLevelInfo) } } + +func IsVerbose() bool { + return verbose +} diff --git a/test/run_test.go b/test/run_test.go index 3b4870c485fc..c5e24fcb04a2 100644 --- a/test/run_test.go +++ b/test/run_test.go @@ -536,86 +536,6 @@ func TestAbsPathFileAnalysis(t *testing.T) { ExpectHasIssue("indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)") } -func TestDisallowedOptionsInConfig(t *testing.T) { - cases := []struct { - cfg string - option string - }{ - { - cfg: ` - ruN: - Args: - - 1 - `, - }, - { - cfg: ` - run: - CPUProfilePath: path - `, - option: "--cpu-profile-path=path", - }, - { - cfg: ` - run: - MemProfilePath: path - `, - option: "--mem-profile-path=path", - }, - { - cfg: ` - run: - TracePath: path - `, - option: "--trace-path=path", - }, - { - cfg: ` - run: - Verbose: true - `, - option: "-v", - }, - } - - testshared.InstallGolangciLint(t) - - for _, c := range cases { - // Run with disallowed option set only in config - testshared.NewRunnerBuilder(t). - WithConfig(c.cfg). - WithTargetPath(testdataDir, minimalPkg). - Runner(). - Run(). - ExpectExitCode(exitcodes.Failure) - - if c.option == "" { - continue - } - - args := []string{c.option, "--fast"} - - // Run with disallowed option set only in command-line - testshared.NewRunnerBuilder(t). - WithNoConfig(). - WithArgs(args...). - WithTargetPath(testdataDir, minimalPkg). - Runner(). - Run(). - ExpectExitCode(exitcodes.Success) - - // Run with disallowed option set both in command-line and in config - - testshared.NewRunnerBuilder(t). - WithConfig(c.cfg). - WithArgs(args...). - WithTargetPath(testdataDir, minimalPkg). - Runner(). - Run(). - ExpectExitCode(exitcodes.Failure) - } -} - func TestPathPrefix(t *testing.T) { testCases := []struct { desc string