diff --git a/.golangci.yml b/.golangci.yml index 214bb4600c95..b56d9fddaf81 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -141,8 +141,6 @@ issues: text: "SA1019: errCfg.Exclude is deprecated: use ExcludeFunctions instead" - path: pkg/commands/run.go text: "SA1019: lsc.Errcheck.Exclude is deprecated: use ExcludeFunctions instead" - - path: pkg/commands/run.go - text: "SA1019: e.cfg.Run.Deadline is deprecated: Deadline exists for historical compatibility and should not be used." - path: pkg/golinters/gofumpt.go text: "SA1019: settings.LangVersion is deprecated: use the global `run.go` instead." diff --git a/pkg/commands/cache.go b/pkg/commands/cache.go index 6fdaebaede8f..3dbe2427f578 100644 --- a/pkg/commands/cache.go +++ b/pkg/commands/cache.go @@ -1,13 +1,19 @@ 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" ) @@ -21,14 +27,13 @@ func (e *Executor) initCache() { return cmd.Help() }, } - e.rootCmd.AddCommand(cacheCmd) cacheCmd.AddCommand(&cobra.Command{ Use: "clean", Short: "Clean cache", Args: cobra.NoArgs, ValidArgsFunction: cobra.NoFileCompletions, - RunE: e.executeCleanCache, + RunE: e.executeCacheClean, }) cacheCmd.AddCommand(&cobra.Command{ Use: "status", @@ -39,9 +44,11 @@ func (e *Executor) initCache() { }) // TODO: add trim command? + + e.rootCmd.AddCommand(cacheCmd) } -func (e *Executor) executeCleanCache(_ *cobra.Command, _ []string) error { +func (e *Executor) executeCacheClean(_ *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) @@ -70,3 +77,68 @@ 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 45c4fcd77cfd..f269d881bd75 100644 --- a/pkg/commands/config.go +++ b/pkg/commands/config.go @@ -14,7 +14,7 @@ import ( ) func (e *Executor) initConfig() { - cmd := &cobra.Command{ + configCmd := &cobra.Command{ Use: "config", Short: "Config file information", Args: cobra.NoArgs, @@ -22,26 +22,34 @@ func (e *Executor) initConfig() { return cmd.Help() }, } - e.rootCmd.AddCommand(cmd) pathCmd := &cobra.Command{ Use: "path", Short: "Print used config path", Args: cobra.NoArgs, ValidArgsFunction: cobra.NoFileCompletions, - Run: e.executePathCmd, + Run: e.executePath, } fs := pathCmd.Flags() fs.SortFlags = false // sort them as they are defined here - initConfigFileFlagSet(fs, &e.cfg.Run) + configCmd.AddCommand(pathCmd) + e.rootCmd.AddCommand(configCmd) +} + +func (e *Executor) executePath(_ *cobra.Command, _ []string) { + usedConfigFile := e.getUsedConfig() + if usedConfigFile == "" { + e.log.Warnf("No config file detected") + os.Exit(exitcodes.NoConfigFileDetected) + } - cmd.AddCommand(pathCmd) + fmt.Println(usedConfigFile) } -// getUsedConfig returns the resolved path to the golangci config file, or the empty string -// if no configuration could be found. +// 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() if usedConfigFile == "" { @@ -57,15 +65,7 @@ func (e *Executor) getUsedConfig() string { return prettyUsedConfigFile } -func (e *Executor) executePathCmd(_ *cobra.Command, _ []string) { - usedConfigFile := e.getUsedConfig() - if usedConfigFile == "" { - e.log.Warnf("No config file detected") - os.Exit(exitcodes.NoConfigFileDetected) - } - - fmt.Println(usedConfigFile) -} +// --- 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`")) diff --git a/pkg/commands/executor.go b/pkg/commands/executor.go index a6e08a48b00d..211466cfb645 100644 --- a/pkg/commands/executor.go +++ b/pkg/commands/executor.go @@ -1,14 +1,9 @@ package commands import ( - "bytes" - "context" - "crypto/sha256" "errors" "fmt" - "io" "os" - "path/filepath" "strings" "time" @@ -16,9 +11,7 @@ import ( "github.com/gofrs/flock" "github.com/spf13/cobra" "github.com/spf13/pflag" - "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/fsutils" @@ -31,54 +24,71 @@ import ( "github.com/golangci/golangci-lint/pkg/timeutils" ) -type BuildInfo struct { - GoVersion string `json:"goVersion"` - Version string `json:"version"` - Commit string `json:"commit"` - Date string `json:"date"` -} - type Executor struct { - rootCmd *cobra.Command - runCmd *cobra.Command - lintersCmd *cobra.Command + rootCmd *cobra.Command + + runCmd *cobra.Command // used by fixSlicesFlags, printStats + lintersCmd *cobra.Command // used by fixSlicesFlags + + exitCode int - exitCode int buildInfo BuildInfo - cfg *config.Config // cfg is the unmarshaled data from the golangci config file. - 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 + 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 { - startedAt := time.Now() e := &Executor{ cfg: config.NewDefault(), buildInfo: buildInfo, - DBManager: lintersdb.NewManager(nil, nil), debugf: logutils.Debug(logutils.DebugKeyExec), } - e.debugf("Starting execution...") e.log = report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), &e.reportData) - // to setup log level early we need to parse config from command line extra time to - // find `-v` option - commandLineCfg, err := e.getConfigForCommandLine() + // 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) } @@ -97,19 +107,8 @@ func NewExecutor(buildInfo BuildInfo) *Executor { } } - // init of commands must be done before config file reading because - // init sets config with the default values of flags - e.initRoot() - e.initRun() - e.initHelp() - e.initLinters() - e.initConfig() - e.initVersion() - e.initCache() - - // 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. + // 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 { @@ -131,135 +130,101 @@ func NewExecutor(buildInfo BuildInfo) *Executor { e.cfg.Run.Go = config.DetectGoVersion() } - // recreate after getting config - e.DBManager = lintersdb.NewManager(e.cfg, e.log) - // 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.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) - e.sw = timeutils.NewStopwatch("pkgcache", e.log.Child(logutils.DebugKeyStopwatch)) - e.pkgCache, err = pkgcache.NewCache(e.sw, e.log.Child(logutils.DebugKeyPkgCache)) + 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.loadGuard = load.NewGuard() + e.contextLoader = lint.NewContextLoader(e.cfg, e.log.Child(logutils.DebugKeyLoader), e.goenv, - e.lineCache, e.fileCache, e.pkgCache, e.loadGuard) - if err = e.initHashSalt(buildInfo.Version); err != nil { + 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) } - e.debugf("Initialized executor in %s", time.Since(startedAt)) - return e } func (e *Executor) Execute() error { return e.rootCmd.Execute() } -func (e *Executor) initHashSalt(version string) error { - binSalt, err := computeBinarySalt(version) - if err != nil { - return fmt.Errorf("failed to calculate binary salt: %w", err) - } - - configSalt, err := computeConfigSalt(e.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 - } +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 + } - if logutils.HaveDebugTag(logutils.DebugKeyBinSalt) { - return []byte("debug"), nil + return nil, fmt.Errorf("can't parse args: %w", err) } - 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 + return &cfg, nil } -func computeConfigSalt(cfg *config.Config) ([]byte, error) { - // We don't hash all config fields to reduce meaningless cache - // invalidations. At least, it has a huge impact on tests speed. - - 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, ",")) +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 + } - h := sha256.New() - if _, err := h.Write(configData.Bytes()); err != nil { - return nil, err - } - return h.Sum(nil), nil -} + s, err := fs.GetStringSlice(f.Name) + if err != nil { + return + } -func (e *Executor) acquireFileLock() bool { - if e.cfg.Run.AllowParallelRunners { - e.debugf("Parallel runners are allowed, no locking") - return true - } + if s == nil { // assume that every string slice flag has nil as the default + return + } - lockFile := filepath.Join(os.TempDir(), "golangci-lint.lock") - e.debugf("Locking on file %s...", lockFile) - f := flock.New(lockFile) - const retryDelay = time.Second - - ctx := context.Background() - if !e.cfg.Run.AllowSerialRunners { - const totalTimeout = 5 * time.Second - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, totalTimeout) - defer cancel() - } - if ok, _ := f.TryLockContext(ctx, retryDelay); !ok { - return false - } + 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+`"`) + } - e.flock = f - return true + // calling Set sets Changed to true: next Set calls will append, not overwrite + _ = f.Value.Set(strings.Join(safe, ",")) + }) } -func (e *Executor) releaseFileLock() { - if e.cfg.Run.AllowParallelRunners { - return - } - - if err := e.flock.Unlock(); err != nil { - e.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) - } +func wh(text string) string { + return color.GreenString(text) } diff --git a/pkg/commands/help.go b/pkg/commands/help.go index a06d508f27c2..016df2c13a56 100644 --- a/pkg/commands/help.go +++ b/pkg/commands/help.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "github.com/golangci/golangci-lint/pkg/lint/linter" + "github.com/golangci/golangci-lint/pkg/lint/lintersdb" "github.com/golangci/golangci-lint/pkg/logutils" ) @@ -21,48 +22,21 @@ func (e *Executor) initHelp() { return cmd.Help() }, } - e.rootCmd.SetHelpCommand(helpCmd) - lintersHelpCmd := &cobra.Command{ + helpCmd.AddCommand(&cobra.Command{ Use: "linters", Short: "Help about linters", Args: cobra.NoArgs, ValidArgsFunction: cobra.NoFileCompletions, - Run: e.executeLintersHelp, - } - helpCmd.AddCommand(lintersHelpCmd) -} - -func printLinterConfigs(lcs []*linter.Config) { - sort.Slice(lcs, func(i, j int) bool { - return lcs[i].Name() < lcs[j].Name() + Run: e.executeHelp, }) - for _, lc := range lcs { - altNamesStr := "" - if len(lc.AlternativeNames) != 0 { - altNamesStr = fmt.Sprintf(" (%s)", strings.Join(lc.AlternativeNames, ", ")) - } - - // If the linter description spans multiple lines, truncate everything following the first newline - linterDescription := lc.Linter.Desc() - firstNewline := strings.IndexRune(linterDescription, '\n') - if firstNewline > 0 { - linterDescription = linterDescription[:firstNewline] - } - - deprecatedMark := "" - if lc.IsDeprecated() { - 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) - } + e.rootCmd.SetHelpCommand(helpCmd) } -func (e *Executor) executeLintersHelp(_ *cobra.Command, _ []string) { +func (e *Executor) executeHelp(_ *cobra.Command, _ []string) { var enabledLCs, disabledLCs []*linter.Config - for _, lc := range e.DBManager.GetAllSupportedLinterConfigs() { + for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() { if lc.Internal { continue } @@ -80,8 +54,8 @@ func (e *Executor) executeLintersHelp(_ *cobra.Command, _ []string) { printLinterConfigs(disabledLCs) color.Green("\nLinters presets:") - for _, p := range e.DBManager.AllPresets() { - linters := e.DBManager.GetAllLinterConfigsForPreset(p) + for _, p := range lintersdb.AllPresets() { + linters := e.dbManager.GetAllLinterConfigsForPreset(p) var linterNames []string for _, lc := range linters { if lc.Internal { @@ -94,3 +68,30 @@ func (e *Executor) executeLintersHelp(_ *cobra.Command, _ []string) { fmt.Fprintf(logutils.StdOut, "%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", ")) } } + +func printLinterConfigs(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 { + altNamesStr = fmt.Sprintf(" (%s)", strings.Join(lc.AlternativeNames, ", ")) + } + + // If the linter description spans multiple lines, truncate everything following the first newline + linterDescription := lc.Linter.Desc() + firstNewline := strings.IndexRune(linterDescription, '\n') + if firstNewline > 0 { + linterDescription = linterDescription[:firstNewline] + } + + deprecatedMark := "" + if lc.IsDeprecated() { + 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) + } +} diff --git a/pkg/commands/linters.go b/pkg/commands/linters.go index 69df211542be..5ed2353fd546 100644 --- a/pkg/commands/linters.go +++ b/pkg/commands/linters.go @@ -10,10 +10,11 @@ import ( "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/lint/linter" + "github.com/golangci/golangci-lint/pkg/lint/lintersdb" ) func (e *Executor) initLinters() { - e.lintersCmd = &cobra.Command{ + lintersCmd := &cobra.Command{ Use: "linters", Short: "List current linters configuration", Args: cobra.NoArgs, @@ -21,29 +22,20 @@ func (e *Executor) initLinters() { RunE: e.executeLinters, } - fs := e.lintersCmd.Flags() + fs := lintersCmd.Flags() fs.SortFlags = false // sort them as they are defined here initConfigFileFlagSet(fs, &e.cfg.Run) - e.initLintersFlagSet(fs, &e.cfg.Linters) + initLintersFlagSet(fs, &e.cfg.Linters) - e.rootCmd.AddCommand(e.lintersCmd) -} + e.rootCmd.AddCommand(lintersCmd) -func (e *Executor) 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(e.DBManager.AllPresets(), "|")))) + e.lintersCmd = lintersCmd } // executeLinters runs the 'linters' CLI command, which displays the supported linters. func (e *Executor) executeLinters(_ *cobra.Command, _ []string) error { - enabledLintersMap, err := e.EnabledLintersSet.GetEnabledLintersMap() + enabledLintersMap, err := e.enabledLintersSet.GetEnabledLintersMap() if err != nil { return fmt.Errorf("can't get enabled linters: %w", err) } @@ -51,7 +43,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 e.dbManager.GetAllSupportedLinterConfigs() { if lc.Internal { continue } @@ -70,3 +62,14 @@ func (e *Executor) executeLinters(_ *cobra.Command, _ []string) error { 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 4425be10fd57..0ae05480fa73 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -22,6 +22,24 @@ const ( envMemProfileRate = "GL_MEM_PROFILE_RATE" ) +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 { + return cmd.Help() + }, + PersistentPreRunE: e.persistentPreRun, + PersistentPostRunE: e.persistentPostRun, + } + + initRootFlagSet(rootCmd.PersistentFlags(), e.cfg) + + e.rootCmd = rootCmd +} + func (e *Executor) persistentPreRun(_ *cobra.Command, _ []string) error { if e.cfg.Run.PrintVersion { _ = printVersion(logutils.StdOut, e.buildInfo) @@ -75,7 +93,7 @@ func (e *Executor) persistentPostRun(_ *cobra.Command, _ []string) error { printMemStats(&ms, e.log) if err := pprof.WriteHeapProfile(f); err != nil { - return fmt.Errorf("cCan't write heap profile: %w", err) + return fmt.Errorf("can't write heap profile: %w", err) } _ = f.Close() } @@ -89,6 +107,20 @@ func (e *Executor) persistentPostRun(_ *cobra.Command, _ []string) error { 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'")) + + 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")) + + fs.IntVarP(&cfg.Run.Concurrency, "concurrency", "j", getDefaultConcurrency(), + wh("Number of CPUs to use (Default: number of logical CPUs)")) + + fs.BoolVar(&cfg.Run.PrintVersion, "version", false, wh("Print version")) +} + 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 "+ @@ -126,50 +158,3 @@ func getDefaultConcurrency() int { return runtime.NumCPU() } - -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 { - return cmd.Help() - }, - PersistentPreRunE: e.persistentPreRun, - PersistentPostRunE: e.persistentPostRun, - } - - initRootFlagSet(rootCmd.PersistentFlags(), e.cfg, e.needVersionOption()) - e.rootCmd = rootCmd -} - -func (e *Executor) needVersionOption() bool { - return e.buildInfo.Date != "" -} - -func initRootFlagSet(fs *pflag.FlagSet, cfg *config.Config, needVersionOption bool) { - fs.BoolVarP(&cfg.Run.IsVerbose, "verbose", "v", false, wh("Verbose output")) - - var silent bool - fs.BoolVarP(&silent, "silent", "s", false, wh("Disables congrats outputs")) - if err := fs.MarkHidden("silent"); err != nil { - panic(err) - } - err := fs.MarkDeprecated("silent", - "now golangci-lint by default is silent: it doesn't print Congrats message") - if err != nil { - panic(err) - } - - 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")) - fs.IntVarP(&cfg.Run.Concurrency, "concurrency", "j", getDefaultConcurrency(), - wh("Number of CPUs to use (Default: number of logical CPUs)")) - if needVersionOption { - fs.BoolVar(&cfg.Run.PrintVersion, "version", false, wh("Print version")) - } - - fs.StringVar(&cfg.Output.Color, "color", "auto", wh("Use color when printing; can be 'always', 'auto', or 'never'")) -} diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 5c7083c307a5..6af4e2863837 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -7,12 +7,14 @@ import ( "io" "log" "os" + "path/filepath" "runtime" "sort" "strings" "time" "github.com/fatih/color" + "github.com/gofrs/flock" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/exp/maps" @@ -38,278 +40,123 @@ const ( envMemLogEvery = "GL_MEM_LOG_EVERY" ) -//nolint:funlen,gomnd -func (e *Executor) initFlagSet(fs *pflag.FlagSet, cfg *config.Config, isFinalInit bool) { - hideFlag := func(name string) { - if err := fs.MarkHidden(name); err != nil { - panic(err) - } - - // we run initFlagSet multiple times, but we wouldn't like to see deprecation message multiple times - if isFinalInit { - const deprecateMessage = "flag will be removed soon, please, use .golangci.yml config" - if err := fs.MarkDeprecated(name, deprecateMessage); err != nil { - panic(err) +func (e *Executor) initRun() { + 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") } - } - } - - // Config file config - rc := &cfg.Run - initConfigFileFlagSet(fs, rc) - - // 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")) - hideFlag("print-welcome") // no longer used - - 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) + return nil + }, + PostRun: func(_ *cobra.Command, _ []string) { + e.releaseFileLock() + }, } - // Run config - 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, "deadline", defaultTimeout, wh("Deadline for total work")) - if err := fs.MarkHidden("deadline"); err != nil { - panic(err) - } - fs.DurationVar(&rc.Timeout, "timeout", defaultTimeout, wh("Timeout for total work")) + runCmd.SetOut(logutils.StdOut) // use custom output to properly color it in Windows terminals + runCmd.SetErr(logutils.StdErr) - 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")) + fs := runCmd.Flags() + fs.SortFlags = false // sort them as they are defined here - 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")) + initRunFlagSet(fs, e.cfg) - // Linters settings config - lsc := &cfg.LintersSettings - - // Hide all linters settings flags: they were initially visible, - // but when number of linters started to grow it became obvious that - // we can't fill 90% of flags by linters settings: common flags became hard to find. - // New linters settings should be done only through config file. - fs.BoolVar(&lsc.Errcheck.CheckTypeAssertions, "errcheck.check-type-assertions", - false, "Errcheck: check for ignored type assertion results") - hideFlag("errcheck.check-type-assertions") - fs.BoolVar(&lsc.Errcheck.CheckAssignToBlank, "errcheck.check-blank", false, - "Errcheck: check for errors assigned to blank identifier: _ = errFunc()") - hideFlag("errcheck.check-blank") - fs.StringVar(&lsc.Errcheck.Exclude, "errcheck.exclude", "", - "Path to a file containing a list of functions to exclude from checking") - hideFlag("errcheck.exclude") - fs.StringVar(&lsc.Errcheck.Ignore, "errcheck.ignore", "fmt:.*", - `Comma-separated list of pairs of the form pkg:regex. The regex is used to ignore names within pkg`) - hideFlag("errcheck.ignore") - - fs.BoolVar(&lsc.Govet.CheckShadowing, "govet.check-shadowing", false, - "Govet: check for shadowed variables") - hideFlag("govet.check-shadowing") - - fs.Float64Var(&lsc.Golint.MinConfidence, "golint.min-confidence", 0.8, - "Golint: minimum confidence of a problem to print it") - hideFlag("golint.min-confidence") - - fs.BoolVar(&lsc.Gofmt.Simplify, "gofmt.simplify", true, "Gofmt: simplify code") - hideFlag("gofmt.simplify") - - fs.IntVar(&lsc.Gocyclo.MinComplexity, "gocyclo.min-complexity", - 30, "Minimal complexity of function to report it") - hideFlag("gocyclo.min-complexity") - - fs.BoolVar(&lsc.Maligned.SuggestNewOrder, "maligned.suggest-new", false, - "Maligned: print suggested more optimal struct fields ordering") - hideFlag("maligned.suggest-new") - - fs.IntVar(&lsc.Dupl.Threshold, "dupl.threshold", - 150, "Dupl: Minimal threshold to detect copy-paste") - hideFlag("dupl.threshold") - - fs.BoolVar(&lsc.Goconst.MatchWithConstants, "goconst.match-constant", - true, "Goconst: look for existing constants matching the values") - hideFlag("goconst.match-constant") - fs.IntVar(&lsc.Goconst.MinStringLen, "goconst.min-len", - 3, "Goconst: minimum constant string length") - hideFlag("goconst.min-len") - fs.IntVar(&lsc.Goconst.MinOccurrencesCount, "goconst.min-occurrences", - 3, "Goconst: minimum occurrences of constant string count to trigger issue") - hideFlag("goconst.min-occurrences") - fs.BoolVar(&lsc.Goconst.ParseNumbers, "goconst.numbers", - false, "Goconst: search also for duplicated numbers") - hideFlag("goconst.numbers") - fs.IntVar(&lsc.Goconst.NumberMin, "goconst.min", - 3, "minimum value, only works with goconst.numbers") - hideFlag("goconst.min") - fs.IntVar(&lsc.Goconst.NumberMax, "goconst.max", - 3, "maximum value, only works with goconst.numbers") - hideFlag("goconst.max") - fs.BoolVar(&lsc.Goconst.IgnoreCalls, "goconst.ignore-calls", - true, "Goconst: ignore when constant is not used as function argument") - hideFlag("goconst.ignore-calls") - - fs.IntVar(&lsc.Lll.TabWidth, "lll.tab-width", 1, - "Lll: tab width in spaces") - hideFlag("lll.tab-width") - - // Linters config - lc := &cfg.Linters - e.initLintersFlagSet(fs, lc) + e.rootCmd.AddCommand(runCmd) - // 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")) + e.runCmd = runCmd +} - 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")) +// 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 + 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 + <-trackResourcesEndCh + } + }() - 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)")) -} + ctx, cancel := context.WithTimeout(context.Background(), e.cfg.Run.Timeout) + defer cancel() -func (e *Executor) initRunConfiguration(cmd *cobra.Command) { - fs := cmd.Flags() - fs.SortFlags = false // sort them as they are defined here - e.initFlagSet(fs, e.cfg, true) -} + if needTrackResources { + go watchResources(ctx, trackResourcesEndCh, e.log, e.debugf) + } -func (e *Executor) 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. - e.initFlagSet(fs, &cfg, false) - 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, true) - - 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 + if err := e.runAndPrint(ctx, args); err != nil { + e.log.Errorf("Running error: %s", err) + if e.exitCode == exitcodes.Success { + var exitErr *exitcodes.ExitError + if errors.As(err, &exitErr) { + e.exitCode = exitErr.Code + } else { + e.exitCode = exitcodes.Failure + } } - - return nil, fmt.Errorf("can't parse args: %w", err) } - return &cfg, nil + e.setupExitCode(ctx) } -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() - }, +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) } - 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) + if !logutils.HaveDebugTag(logutils.DebugKeyLintersOutput) { + // Don't allow linters and loader to print anything + log.SetOutput(io.Discard) + savedStdout, savedStderr := e.setOutputToDevNull() + defer func() { + os.Stdout, os.Stderr = savedStdout, savedStderr + }() + } - e.initRunConfiguration(e.runCmd) -} + issues, err := e.runAnalysis(ctx, args) + if err != nil { + return err // XXX: don't loose type + } -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 + formats := strings.Split(e.cfg.Output.Format, ",") + for _, format := range formats { + out := strings.SplitN(format, ":", 2) + if len(out) < 2 { + out = append(out, "") } - s, err := fs.GetStringSlice(f.Name) + err := e.printReports(issues, out[1], out[0]) if err != nil { - return + return err } + } - if s == nil { // assume that every string slice flag has nil as the default - return - } + e.printStats(issues) - 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+`"`) - } + e.setExitCodeIfIssuesFound(issues) + + e.fileCache.PrintStats(e.log) - // calling Set sets Changed to true: next Set calls will append, not overwrite - _ = f.Value.Set(strings.Join(safe, ",")) - }) + 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 - lintersToRun, err := e.EnabledLintersSet.GetOptimizedLinters() + lintersToRun, err := e.enabledLintersSet.GetOptimizedLinters() if err != nil { return nil, err } - enabledLintersMap, err := e.EnabledLintersSet.GetEnabledLintersMap() + enabledLintersMap, err := e.enabledLintersSet.GetEnabledLintersMap() if err != nil { return nil, err } - for _, lc := range e.DBManager.GetAllSupportedLinterConfigs() { + for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() { isEnabled := enabledLintersMap[lc.Name()] != nil e.reportData.AddLinter(lc.Name(), isEnabled, lc.EnabledByDefault) } @@ -321,7 +168,7 @@ func (e *Executor) runAnalysis(ctx context.Context, args []string) ([]result.Iss lintCtx.Log = e.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) + e.goenv, e.enabledLintersSet, e.lineCache, e.fileCache, e.dbManager, lintCtx.Packages) if err != nil { return nil, err } @@ -347,47 +194,6 @@ func (e *Executor) setExitCodeIfIssuesFound(issues []result.Issue) { } } -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) - } - - if !logutils.HaveDebugTag(logutils.DebugKeyLintersOutput) { - // Don't allow linters and loader to print anything - log.SetOutput(io.Discard) - savedStdout, savedStderr := e.setOutputToDevNull() - defer func() { - os.Stdout, os.Stderr = savedStdout, savedStderr - }() - } - - issues, err := e.runAnalysis(ctx, args) - if err != nil { - return err // XXX: don't loose type - } - - formats := strings.Split(e.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]) - if err != nil { - return err - } - } - - e.printStats(issues) - - e.setExitCodeIfIssuesFound(issues) - - e.fileCache.PrintStats(e.log) - - return nil -} - func (e *Executor) printReports(issues []result.Issue, path, format string) error { w, shouldClose, err := e.createWriter(path) if err != nil { @@ -487,47 +293,6 @@ func (e *Executor) printStats(issues []result.Issue) { } } -// 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 - 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 - <-trackResourcesEndCh - } - }() - - e.setTimeoutToDeadlineIfOnlyDeadlineIsSet() - ctx, cancel := context.WithTimeout(context.Background(), e.cfg.Run.Timeout) - defer cancel() - - if needTrackResources { - go watchResources(ctx, trackResourcesEndCh, e.log, e.debugf) - } - - if err := e.runAndPrint(ctx, args); err != nil { - e.log.Errorf("Running error: %s", err) - if e.exitCode == exitcodes.Success { - var exitErr *exitcodes.ExitError - if errors.As(err, &exitErr) { - e.exitCode = exitErr.Code - } else { - e.exitCode = exitcodes.Failure - } - } - } - - e.setupExitCode(ctx) -} - -// to be removed when deadline is finally decommissioned -func (e *Executor) setTimeoutToDeadlineIfOnlyDeadlineIsSet() { - deadlineValue := e.cfg.Run.Deadline - if deadlineValue != 0 && e.cfg.Run.Timeout == defaultTimeout { - e.cfg.Run.Timeout = deadlineValue - } -} - func (e *Executor) setupExitCode(ctx context.Context) { if ctx.Err() != nil { e.exitCode = exitcodes.Timeout @@ -552,6 +317,130 @@ func (e *Executor) setupExitCode(ctx context.Context) { } } +func (e *Executor) acquireFileLock() bool { + if e.cfg.Run.AllowParallelRunners { + e.debugf("Parallel runners are allowed, no locking") + return true + } + + lockFile := filepath.Join(os.TempDir(), "golangci-lint.lock") + e.debugf("Locking on file %s...", lockFile) + f := flock.New(lockFile) + const retryDelay = time.Second + + ctx := context.Background() + if !e.cfg.Run.AllowSerialRunners { + const totalTimeout = 5 * time.Second + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, totalTimeout) + defer cancel() + } + if ok, _ := f.TryLockContext(ctx, retryDelay); !ok { + return false + } + + e.flock = f + return true +} + +func (e *Executor) releaseFileLock() { + if e.cfg.Run.AllowParallelRunners { + return + } + + if err := e.flock.Unlock(); err != nil { + e.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) + } +} + +//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") @@ -628,7 +517,3 @@ func getDefaultDirectoryExcludeHelp() string { parts = append(parts, "") return strings.Join(parts, "\n") } - -func wh(text string) string { - return color.GreenString(text) -} diff --git a/pkg/commands/version.go b/pkg/commands/version.go index 10ab7c1bb1b1..18e5716b7e90 100644 --- a/pkg/commands/version.go +++ b/pkg/commands/version.go @@ -14,66 +14,73 @@ import ( "github.com/golangci/golangci-lint/pkg/config" ) +type BuildInfo struct { + GoVersion string `json:"goVersion"` + Version string `json:"version"` + Commit string `json:"commit"` + Date string `json:"date"` +} + type versionInfo struct { Info BuildInfo BuildInfo *debug.BuildInfo } -func (e *Executor) initVersionConfiguration(cmd *cobra.Command) { - fs := cmd.Flags() - fs.SortFlags = false // sort them as they are defined here - initVersionFlagSet(fs, e.cfg) -} - -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 (e *Executor) initVersion() { versionCmd := &cobra.Command{ Use: "version", Short: "Version", Args: cobra.NoArgs, ValidArgsFunction: cobra.NoFileCompletions, - RunE: func(_ *cobra.Command, _ []string) error { - if e.cfg.Version.Debug { - info, ok := debug.ReadBuildInfo() - if !ok { - return nil - } - - switch strings.ToLower(e.cfg.Version.Format) { - case "json": - return json.NewEncoder(os.Stdout).Encode(versionInfo{ - Info: e.buildInfo, - BuildInfo: info, - }) - - default: - fmt.Println(info.String()) - return printVersion(os.Stdout, e.buildInfo) - } - } - - switch strings.ToLower(e.cfg.Version.Format) { - case "short": - fmt.Println(e.buildInfo.Version) - return nil - - case "json": - return json.NewEncoder(os.Stdout).Encode(e.buildInfo) - - default: - return printVersion(os.Stdout, e.buildInfo) - } - }, + RunE: e.executeVersion, } + fs := versionCmd.Flags() + fs.SortFlags = false // sort them as they are defined here + + initVersionFlagSet(fs, e.cfg) + e.rootCmd.AddCommand(versionCmd) - e.initVersionConfiguration(versionCmd) +} + +func (e *Executor) executeVersion(_ *cobra.Command, _ []string) error { + if e.cfg.Version.Debug { + info, ok := debug.ReadBuildInfo() + if !ok { + return nil + } + + switch strings.ToLower(e.cfg.Version.Format) { + case "json": + return json.NewEncoder(os.Stdout).Encode(versionInfo{ + Info: e.buildInfo, + BuildInfo: info, + }) + + default: + fmt.Println(info.String()) + return printVersion(os.Stdout, e.buildInfo) + } + } + + switch strings.ToLower(e.cfg.Version.Format) { + case "short": + fmt.Println(e.buildInfo.Version) + return nil + + case "json": + return json.NewEncoder(os.Stdout).Encode(e.buildInfo) + + default: + return printVersion(os.Stdout, e.buildInfo) + } +} + +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 { diff --git a/pkg/config/config.go b/pkg/config/config.go index 2eb82938dc69..f73563a2a4ef 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -8,20 +8,22 @@ import ( "github.com/ldez/gomoddirectives" ) -// Config encapsulates the config data specified in the golangci yaml config file. +// Config encapsulates the config data specified in the golangci-lint yaml config file. type Config struct { - cfgDir string // The directory containing the golangci config file. - Run Run + cfgDir string // The directory containing the golangci-lint config file. - Output Output + Run Run `mapstructure:"run"` + + Output Output `mapstructure:"output"` LintersSettings LintersSettings `mapstructure:"linters-settings"` - Linters Linters - Issues Issues - Severity Severity - Version Version + Linters Linters `mapstructure:"linters"` + Issues Issues `mapstructure:"issues"` + Severity Severity `mapstructure:"severity"` + + Version Version // Flag only. // TODO(ldez) only used by the version command. - InternalCmdTest bool `mapstructure:"internal-cmd-test"` // Option is used only for testing golangci-lint command, don't use it + 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 } diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 701beb2241b0..57d126d940e1 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -21,6 +21,12 @@ var defaultLintersSettings = LintersSettings{ Dogsled: DogsledSettings{ MaxBlankIdentifiers: 2, }, + Dupl: DuplSettings{ + Threshold: 150, + }, + Errcheck: ErrcheckSettings{ + Ignore: "fmt:.*", + }, ErrorLint: ErrorLintSettings{ Errorf: true, ErrorfMulti: true, @@ -46,9 +52,20 @@ var defaultLintersSettings = LintersSettings{ Gocognit: GocognitSettings{ MinComplexity: 30, }, + Goconst: GoConstSettings{ + MatchWithConstants: true, + MinStringLen: 3, + MinOccurrencesCount: 3, + NumberMin: 3, + NumberMax: 3, + IgnoreCalls: true, + }, Gocritic: GoCriticSettings{ SettingsPerCheck: map[string]GoCriticCheckSettings{}, }, + Gocyclo: GoCycloSettings{ + MinComplexity: 30, + }, Godox: GodoxSettings{ Keywords: []string{}, }, @@ -56,11 +73,17 @@ var defaultLintersSettings = LintersSettings{ Scope: "declarations", Period: true, }, + Gofmt: GoFmtSettings{ + Simplify: true, + }, Gofumpt: GofumptSettings{ LangVersion: "", ModulePath: "", ExtraRules: false, }, + Golint: GoLintSettings{ + MinConfidence: 0.8, + }, Gosec: GoSecSettings{ Concurrency: runtime.NumCPU(), }, @@ -583,6 +606,7 @@ type GovetSettings struct { } func (cfg *GovetSettings) Validate() error { + // TODO(ldez) need to be move into the linter file. if cfg.EnableAll && cfg.DisableAll { return errors.New("enable-all and disable-all can't be combined") } diff --git a/pkg/config/output.go b/pkg/config/output.go index e8726392055d..28e4f29b3414 100644 --- a/pkg/config/output.go +++ b/pkg/config/output.go @@ -28,7 +28,7 @@ var OutFormats = []string{ } type Output struct { - Format string + Format string `mapstructure:"format"` PrintIssuedLine bool `mapstructure:"print-issued-lines"` PrintLinterName bool `mapstructure:"print-linter-name"` UniqByLine bool `mapstructure:"uniq-by-line"` @@ -37,5 +37,5 @@ type Output struct { PathPrefix string `mapstructure:"path-prefix"` // only work with CLI flags because the setup of logs is done before the config file parsing. - Color string + Color string // Flag only. } diff --git a/pkg/config/reader.go b/pkg/config/reader.go index 0c4fa13b0636..40ff6f0e1c68 100644 --- a/pkg/config/reader.go +++ b/pkg/config/reader.go @@ -61,7 +61,8 @@ func (r *FileReader) Read() error { func (r *FileReader) parseConfig() error { if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { + var configFileNotFoundError viper.ConfigFileNotFoundError + if errors.As(err, &configFileNotFoundError) { return nil } @@ -116,42 +117,41 @@ func (r *FileReader) parseConfig() error { } func (r *FileReader) validateConfig() error { - c := r.cfg - if len(c.Run.Args) != 0 { + if len(r.cfg.Run.Args) != 0 { return errors.New("option run.args in config isn't supported now") } - if c.Run.CPUProfilePath != "" { + if r.cfg.Run.CPUProfilePath != "" { return errors.New("option run.cpuprofilepath in config isn't allowed") } - if c.Run.MemProfilePath != "" { + if r.cfg.Run.MemProfilePath != "" { return errors.New("option run.memprofilepath in config isn't allowed") } - if c.Run.TracePath != "" { + if r.cfg.Run.TracePath != "" { return errors.New("option run.tracepath in config isn't allowed") } - if c.Run.IsVerbose { + if r.cfg.Run.IsVerbose { return errors.New("can't set run.verbose option with config: only on command-line") } - for i, rule := range c.Issues.ExcludeRules { + + 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(c.Severity.Rules) > 0 && c.Severity.Default == "" { + + 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 c.Severity.Rules { + 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) } } - if err := c.LintersSettings.Govet.Validate(); err != nil { - return fmt.Errorf("error in govet config: %w", err) - } + return nil } diff --git a/pkg/config/run.go b/pkg/config/run.go index 2bb21d9fa224..52c6ce262b57 100644 --- a/pkg/config/run.go +++ b/pkg/config/run.go @@ -4,18 +4,9 @@ import "time" // Run encapsulates the config options for running the linter analysis. type Run struct { - IsVerbose bool `mapstructure:"verbose"` - Silent bool - CPUProfilePath string - MemProfilePath string - TracePath string - Concurrency int - PrintResourcesUsage bool `mapstructure:"print-resources-usage"` + Timeout time.Duration `mapstructure:"timeout"` - Config string // The path to the golangci config file, as specified with the --config argument. - NoConfig bool - - Args []string + Concurrency int `mapstructure:"concurrency"` Go string `mapstructure:"go"` @@ -25,12 +16,6 @@ type Run struct { ExitCodeIfIssuesFound int `mapstructure:"issues-exit-code"` AnalyzeTests bool `mapstructure:"tests"` - // Deprecated: Deadline exists for historical compatibility - // and should not be used. To set run timeout use Timeout instead. - Deadline time.Duration - Timeout time.Duration - - PrintVersion bool SkipFiles []string `mapstructure:"skip-files"` SkipDirs []string `mapstructure:"skip-dirs"` UseDefaultSkipDirs bool `mapstructure:"skip-dirs-use-default"` @@ -39,4 +24,21 @@ type Run struct { AllowSerialRunners bool `mapstructure:"allow-serial-runners"` 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. } diff --git a/pkg/golinters/gochecknoglobals.go b/pkg/golinters/gochecknoglobals.go index 6e18aeb27d90..d1edd48f6408 100644 --- a/pkg/golinters/gochecknoglobals.go +++ b/pkg/golinters/gochecknoglobals.go @@ -8,22 +8,19 @@ import ( ) func NewGochecknoglobals() *goanalysis.Linter { - gochecknoglobals := checknoglobals.Analyzer() + a := checknoglobals.Analyzer() - // gochecknoglobals only lints test files if the `-t` flag is passed, so we - // pass the `t` flag as true to the analyzer before running it. This can be - // turned off by using the regular golangci-lint flags such as `--tests` or - // `--skip-files`. + // gochecknoglobals only lints test files if the `-t` flag is passed, + // so we pass the `t` flag as true to the analyzer before running it. + // This can be turned off by using the regular golangci-lint flags such as `--tests` or `--skip-files`. linterConfig := map[string]map[string]any{ - gochecknoglobals.Name: { - "t": true, - }, + a.Name: {"t": true}, } return goanalysis.NewLinter( - gochecknoglobals.Name, - gochecknoglobals.Doc, - []*analysis.Analyzer{gochecknoglobals}, + a.Name, + a.Doc, + []*analysis.Analyzer{a}, linterConfig, ).WithLoadMode(goanalysis.LoadModeTypesInfo) } diff --git a/pkg/golinters/govet.go b/pkg/golinters/govet.go index 066f7e682a3d..2ed8f252f606 100644 --- a/pkg/golinters/govet.go +++ b/pkg/golinters/govet.go @@ -139,6 +139,11 @@ var ( func NewGovet(settings *config.GovetSettings) *goanalysis.Linter { var conf map[string]map[string]any if settings != nil { + err := settings.Validate() + if err != nil { + linterLogger.Fatalf("govet configuration: %v", err) + } + conf = settings.Settings } diff --git a/pkg/goutil/env.go b/pkg/goutil/env.go index 93922f85a717..7b748d8e9031 100644 --- a/pkg/goutil/env.go +++ b/pkg/goutil/env.go @@ -33,20 +33,23 @@ func NewEnv(log logutils.Log) *Env { } } -func (e *Env) Discover(ctx context.Context) error { +func (e Env) Discover(ctx context.Context) error { startedAt := time.Now() - args := []string{"env", "-json"} - args = append(args, string(EnvGoCache), string(EnvGoRoot)) - out, err := exec.CommandContext(ctx, "go", args...).Output() + + //nolint:gosec // Everything is static here. + cmd := exec.CommandContext(ctx, "go", "env", "-json", string(EnvGoCache), string(EnvGoRoot)) + + out, err := cmd.Output() if err != nil { - return fmt.Errorf("failed to run 'go env': %w", err) + return fmt.Errorf("failed to run '%s': %w", strings.Join(cmd.Args, " "), err) } if err = json.Unmarshal(out, &e.vars); err != nil { - return fmt.Errorf("failed to parse 'go %s' json: %w", strings.Join(args, " "), err) + return fmt.Errorf("failed to parse '%s' json: %w", strings.Join(cmd.Args, " "), err) } e.debugf("Read go env for %s: %#v", time.Since(startedAt), e.vars) + return nil } diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index e6a171f1ff5d..707a0bd15664 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/pkg/lint/lintersdb/manager.go @@ -33,38 +33,12 @@ func NewManager(cfg *config.Config, log logutils.Log) *Manager { return m } -func (Manager) AllPresets() []string { - return []string{ - linter.PresetBugs, - linter.PresetComment, - linter.PresetComplexity, - linter.PresetError, - linter.PresetFormatting, - linter.PresetImport, - linter.PresetMetaLinter, - linter.PresetModule, - linter.PresetPerformance, - linter.PresetSQL, - linter.PresetStyle, - linter.PresetTest, - linter.PresetUnused, - } -} - -func (m Manager) allPresetsSet() map[string]bool { - ret := map[string]bool{} - for _, p := range m.AllPresets() { - ret[p] = true - } - return ret -} - -func (m Manager) GetLinterConfigs(name string) []*linter.Config { +func (m *Manager) GetLinterConfigs(name string) []*linter.Config { return m.nameToLCs[name] } //nolint:funlen -func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { +func (m *Manager) GetAllSupportedLinterConfigs() []*linter.Config { var ( asasalintCfg *config.AsasalintSettings bidichkCfg *config.BiDiChkSettings @@ -953,7 +927,7 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { return linters } -func (m Manager) GetAllEnabledByDefaultLinters() []*linter.Config { +func (m *Manager) GetAllEnabledByDefaultLinters() []*linter.Config { var ret []*linter.Config for _, lc := range m.GetAllSupportedLinterConfigs() { if lc.EnabledByDefault { @@ -964,17 +938,7 @@ func (m Manager) GetAllEnabledByDefaultLinters() []*linter.Config { return ret } -func linterConfigsToMap(lcs []*linter.Config) map[string]*linter.Config { - ret := map[string]*linter.Config{} - for _, lc := range lcs { - lc := lc // local copy - ret[lc.Name()] = lc - } - - return ret -} - -func (m Manager) GetAllLinterConfigsForPreset(p string) []*linter.Config { +func (m *Manager) GetAllLinterConfigsForPreset(p string) []*linter.Config { var ret []*linter.Config for _, lc := range m.GetAllSupportedLinterConfigs() { if lc.IsDeprecated() { @@ -992,6 +956,34 @@ func (m Manager) GetAllLinterConfigsForPreset(p string) []*linter.Config { return ret } +func linterConfigsToMap(lcs []*linter.Config) map[string]*linter.Config { + ret := map[string]*linter.Config{} + for _, lc := range lcs { + lc := lc // local copy + ret[lc.Name()] = lc + } + + return ret +} + +func AllPresets() []string { + return []string{ + linter.PresetBugs, + linter.PresetComment, + linter.PresetComplexity, + linter.PresetError, + linter.PresetFormatting, + linter.PresetImport, + linter.PresetMetaLinter, + linter.PresetModule, + linter.PresetPerformance, + linter.PresetSQL, + linter.PresetStyle, + linter.PresetTest, + linter.PresetUnused, + } +} + // Trims the Go version to keep only M.m. // Since Go 1.21 the version inside the go.mod can be a patched version (ex: 1.21.0). // https://go.dev/doc/toolchain#versions diff --git a/pkg/lint/lintersdb/validator.go b/pkg/lint/lintersdb/validator.go index 52a70d85900d..9ea568f1a44e 100644 --- a/pkg/lint/lintersdb/validator.go +++ b/pkg/lint/lintersdb/validator.go @@ -3,6 +3,7 @@ package lintersdb import ( "errors" "fmt" + "slices" "strings" "github.com/golangci/golangci-lint/pkg/config" @@ -39,11 +40,12 @@ func (v Validator) validateLintersNames(cfg *config.Linters) error { } func (v Validator) validatePresets(cfg *config.Linters) error { - allPresets := v.m.allPresetsSet() + presets := AllPresets() + for _, p := range cfg.Presets { - if !allPresets[p] { + if !slices.Contains(presets, p) { return fmt.Errorf("no such preset %q: only next presets exist: (%s)", - p, strings.Join(v.m.AllPresets(), "|")) + p, strings.Join(presets, "|")) } } diff --git a/test/bench/bench_test.go b/test/bench/bench_test.go index a6ef3fe2184e..bd7530b3e2f3 100644 --- a/test/bench/bench_test.go +++ b/test/bench/bench_test.go @@ -76,7 +76,7 @@ func printCommand(cmd string, args ...string) { } func getGolangciLintCommonArgs() []string { - return []string{"run", "--no-config", "--issues-exit-code=0", "--deadline=30m", "--disable-all", "--enable=govet"} + return []string{"run", "--no-config", "--issues-exit-code=0", "--timeout=30m", "--disable-all", "--enable=govet"} } func runGolangciLintForBench(b testing.TB) { diff --git a/test/run_test.go b/test/run_test.go index 9fad30e13dc2..3b4870c485fc 100644 --- a/test/run_test.go +++ b/test/run_test.go @@ -54,20 +54,6 @@ func TestSymlinkLoop(t *testing.T) { ExpectNoIssues() } -// TODO(ldez): remove this in v2. -func TestDeadline(t *testing.T) { - projectRoot := filepath.Join("..", "...") - - testshared.NewRunnerBuilder(t). - WithArgs("--deadline=1ms"). - WithTargetPath(projectRoot). - Runner(). - Install(). - Run(). - ExpectExitCode(exitcodes.Timeout). - ExpectOutputContains(`Timeout exceeded: try increasing it by passing --timeout option`) -} - func TestTimeout(t *testing.T) { projectRoot := filepath.Join("..", "...") @@ -82,43 +68,21 @@ func TestTimeout(t *testing.T) { } func TestTimeoutInConfig(t *testing.T) { - cases := []struct { - cfg string - }{ - { - cfg: ` - run: - deadline: 1ms - `, - }, - { - cfg: ` - run: - timeout: 1ms - `, - }, - { - // timeout should override deadline - cfg: ` + testshared.InstallGolangciLint(t) + + cfg := ` run: - deadline: 100s timeout: 1ms - `, - }, - } - - 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.Timeout). - ExpectOutputContains(`Timeout exceeded: try increasing it by passing --timeout option`) - } + // Run with disallowed option set only in config + testshared.NewRunnerBuilder(t). + WithConfig(cfg). + WithTargetPath(testdataDir, minimalPkg). + Runner(). + Run(). + ExpectExitCode(exitcodes.Timeout). + ExpectOutputContains(`Timeout exceeded: try increasing it by passing --timeout option`) } func TestTestsAreLintedByDefault(t *testing.T) {