dev: clean up Executor ()

This commit is contained in:
Ludovic Fernandez 2024-02-23 20:38:42 +01:00 committed by GitHub
parent 4068bb73fe
commit 8c51979ec8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 716 additions and 809 deletions

View file

@ -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
}

View file

@ -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)
cmd.AddCommand(pathCmd)
configCmd.AddCommand(pathCmd)
e.rootCmd.AddCommand(configCmd)
}
// getUsedConfig returns the resolved path to the golangci config file, or the empty string
// if no configuration could be found.
func (e *Executor) executePath(_ *cobra.Command, _ []string) {
usedConfigFile := e.getUsedConfig()
if usedConfigFile == "" {
e.log.Warnf("No config file detected")
os.Exit(exitcodes.NoConfigFileDetected)
}
fmt.Println(usedConfigFile)
}
// 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`"))

View file

@ -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
cfg *config.Config // cfg is the unmarshaled data from the golangci config file.
loadGuard *load.Guard
flock *flock.Flock
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)
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)
}
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
return &cfg, nil
}
func computeBinarySalt(version string) ([]byte, error) {
if version != "" && version != "(devel)" {
return []byte(version), 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
}
if logutils.HaveDebugTag(logutils.DebugKeyBinSalt) {
return []byte("debug"), nil
}
s, err := fs.GetStringSlice(f.Name)
if err != nil {
return
}
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
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 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, ","))
h := sha256.New()
if _, err := h.Write(configData.Bytes()); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
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)
}
func wh(text string) string {
return color.GreenString(text)
}

View file

@ -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,16 +22,51 @@ 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,
Run: e.executeHelp,
})
e.rootCmd.SetHelpCommand(helpCmd)
}
func (e *Executor) executeHelp(_ *cobra.Command, _ []string) {
var enabledLCs, disabledLCs []*linter.Config
for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() {
if lc.Internal {
continue
}
if lc.EnabledByDefault {
enabledLCs = append(enabledLCs, lc)
} else {
disabledLCs = append(disabledLCs, lc)
}
}
color.Green("Enabled by default linters:\n")
printLinterConfigs(enabledLCs)
color.Red("\nDisabled by default linters:\n")
printLinterConfigs(disabledLCs)
color.Green("\nLinters presets:")
for _, p := range lintersdb.AllPresets() {
linters := e.dbManager.GetAllLinterConfigsForPreset(p)
var linterNames []string
for _, lc := range linters {
if lc.Internal {
continue
}
linterNames = append(linterNames, lc.Name())
}
sort.Strings(linterNames)
fmt.Fprintf(logutils.StdOut, "%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", "))
}
helpCmd.AddCommand(lintersHelpCmd)
}
func printLinterConfigs(lcs []*linter.Config) {
@ -59,38 +95,3 @@ func printLinterConfigs(lcs []*linter.Config) {
altNamesStr, deprecatedMark, linterDescription, !lc.IsSlowLinter(), lc.CanAutoFix)
}
}
func (e *Executor) executeLintersHelp(_ *cobra.Command, _ []string) {
var enabledLCs, disabledLCs []*linter.Config
for _, lc := range e.DBManager.GetAllSupportedLinterConfigs() {
if lc.Internal {
continue
}
if lc.EnabledByDefault {
enabledLCs = append(enabledLCs, lc)
} else {
disabledLCs = append(disabledLCs, lc)
}
}
color.Green("Enabled by default linters:\n")
printLinterConfigs(enabledLCs)
color.Red("\nDisabled by default linters:\n")
printLinterConfigs(disabledLCs)
color.Green("\nLinters presets:")
for _, p := range e.DBManager.AllPresets() {
linters := e.DBManager.GetAllLinterConfigsForPreset(p)
var linterNames []string
for _, lc := range linters {
if lc.Internal {
continue
}
linterNames = append(linterNames, lc.Name())
}
sort.Strings(linterNames)
fmt.Fprintf(logutils.StdOut, "%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", "))
}
}

View file

@ -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(), "|"))))
}

View file

@ -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'"))
}

View file

@ -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,212 +40,8 @@ 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)
}
}
}
// 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)
}
// Run config
fs.StringVar(&rc.ModulesDownloadMode, "modules-download-mode", "",
wh("Modules download mode. If not empty, passed as -mod=<mode> 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"))
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 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)
// 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 (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)
}
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
}
return nil, fmt.Errorf("can't parse args: %w", err)
}
return &cfg, nil
}
func (e *Executor) initRun() {
e.runCmd = &cobra.Command{
runCmd := &cobra.Command{
Use: "run",
Short: "Run the linters",
Run: e.executeRun,
@ -257,94 +55,50 @@ func (e *Executor) initRun() {
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)
runCmd.SetOut(logutils.StdOut) // use custom output to properly color it in Windows terminals
runCmd.SetErr(logutils.StdErr)
e.initRunConfiguration(e.runCmd)
fs := runCmd.Flags()
fs.SortFlags = false // sort them as they are defined here
initRunFlagSet(fs, e.cfg)
e.rootCmd.AddCommand(runCmd)
e.runCmd = runCmd
}
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
// 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
}
}()
s, err := fs.GetStringSlice(f.Name)
if err != nil {
return
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
}
}
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, ","))
})
}
// 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()
if err != nil {
return nil, err
}
enabledLintersMap, err := e.EnabledLintersSet.GetEnabledLintersMap()
if err != nil {
return nil, err
}
for _, lc := range e.DBManager.GetAllSupportedLinterConfigs() {
isEnabled := enabledLintersMap[lc.Name()] != nil
e.reportData.AddLinter(lc.Name(), isEnabled, lc.EnabledByDefault)
}
lintCtx, err := e.contextLoader.Load(ctx, lintersToRun)
if err != nil {
return nil, fmt.Errorf("context loading failed: %w", err)
}
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)
if err != nil {
return nil, err
}
return runner.Run(ctx, lintersToRun, lintCtx)
}
func (e *Executor) 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)
return
}
os.Stdout, os.Stderr = devNull, devNull
return
}
func (e *Executor) setExitCodeIfIssuesFound(issues []result.Issue) {
if len(issues) != 0 {
e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound
}
e.setupExitCode(ctx)
}
func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
@ -388,6 +142,58 @@ func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
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()
if err != nil {
return nil, err
}
enabledLintersMap, err := e.enabledLintersSet.GetEnabledLintersMap()
if err != nil {
return nil, err
}
for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() {
isEnabled := enabledLintersMap[lc.Name()] != nil
e.reportData.AddLinter(lc.Name(), isEnabled, lc.EnabledByDefault)
}
lintCtx, err := e.contextLoader.Load(ctx, lintersToRun)
if err != nil {
return nil, fmt.Errorf("context loading failed: %w", err)
}
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)
if err != nil {
return nil, err
}
return runner.Run(ctx, lintersToRun, lintCtx)
}
func (e *Executor) 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)
return
}
os.Stdout, os.Stderr = devNull, devNull
return
}
func (e *Executor) setExitCodeIfIssuesFound(issues []result.Issue) {
if len(issues) != 0 {
e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound
}
}
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=<mode> 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)
}

View file

@ -14,15 +14,66 @@ 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()
func (e *Executor) initVersion() {
versionCmd := &cobra.Command{
Use: "version",
Short: "Version",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: e.executeVersion,
}
fs := versionCmd.Flags()
fs.SortFlags = false // sort them as they are defined here
initVersionFlagSet(fs, e.cfg)
e.rootCmd.AddCommand(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) {
@ -32,50 +83,6 @@ func initVersionFlagSet(fs *pflag.FlagSet, cfg *config.Config) {
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)
}
},
}
e.rootCmd.AddCommand(versionCmd)
e.initVersionConfiguration(versionCmd)
}
func printVersion(w io.Writer, buildInfo 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)