dev: new commands system ()

This commit is contained in:
Ludovic Fernandez 2024-02-27 00:03:48 +01:00 committed by GitHub
parent b5d7302867
commit 784264d72e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1189 additions and 1153 deletions

View file

@ -1,24 +1,24 @@
package commands
import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/golangci/golangci-lint/internal/cache"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/logutils"
)
func (e *Executor) initCache() {
type cacheCommand struct {
cmd *cobra.Command
}
func newCacheCommand() *cacheCommand {
c := &cacheCommand{}
cacheCmd := &cobra.Command{
Use: "cache",
Short: "Cache control and information",
@ -28,28 +28,31 @@ func (e *Executor) initCache() {
},
}
cacheCmd.AddCommand(&cobra.Command{
Use: "clean",
Short: "Clean cache",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: e.executeCacheClean,
})
cacheCmd.AddCommand(&cobra.Command{
Use: "status",
Short: "Show cache status",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
Run: e.executeCacheStatus,
})
cacheCmd.AddCommand(
&cobra.Command{
Use: "clean",
Short: "Clean cache",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: c.executeClean,
},
&cobra.Command{
Use: "status",
Short: "Show cache status",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
Run: c.executeStatus,
},
)
// TODO: add trim command?
c.cmd = cacheCmd
e.rootCmd.AddCommand(cacheCmd)
return c
}
func (e *Executor) executeCacheClean(_ *cobra.Command, _ []string) error {
func (c *cacheCommand) executeClean(_ *cobra.Command, _ []string) error {
cacheDir := cache.DefaultDir()
if err := os.RemoveAll(cacheDir); err != nil {
return fmt.Errorf("failed to remove dir %s: %w", cacheDir, err)
}
@ -57,13 +60,13 @@ func (e *Executor) executeCacheClean(_ *cobra.Command, _ []string) error {
return nil
}
func (e *Executor) executeCacheStatus(_ *cobra.Command, _ []string) {
func (c *cacheCommand) executeStatus(_ *cobra.Command, _ []string) {
cacheDir := cache.DefaultDir()
fmt.Fprintf(logutils.StdOut, "Dir: %s\n", cacheDir)
_, _ = fmt.Fprintf(logutils.StdOut, "Dir: %s\n", cacheDir)
cacheSizeBytes, err := dirSizeBytes(cacheDir)
if err == nil {
fmt.Fprintf(logutils.StdOut, "Size: %s\n", fsutils.PrettifyBytesCount(cacheSizeBytes))
_, _ = fmt.Fprintf(logutils.StdOut, "Size: %s\n", fsutils.PrettifyBytesCount(cacheSizeBytes))
}
}
@ -77,68 +80,3 @@ func dirSizeBytes(path string) (int64, error) {
})
return size, err
}
// --- Related to cache but not used directly by the cache command.
func initHashSalt(version string, cfg *config.Config) error {
binSalt, err := computeBinarySalt(version)
if err != nil {
return fmt.Errorf("failed to calculate binary salt: %w", err)
}
configSalt, err := computeConfigSalt(cfg)
if err != nil {
return fmt.Errorf("failed to calculate config salt: %w", err)
}
b := bytes.NewBuffer(binSalt)
b.Write(configSalt)
cache.SetSalt(b.Bytes())
return nil
}
func computeBinarySalt(version string) ([]byte, error) {
if version != "" && version != "(devel)" {
return []byte(version), nil
}
if logutils.HaveDebugTag(logutils.DebugKeyBinSalt) {
return []byte("debug"), nil
}
p, err := os.Executable()
if err != nil {
return nil, err
}
f, err := os.Open(p)
if err != nil {
return nil, err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// computeConfigSalt computes configuration hash.
// We don't hash all config fields to reduce meaningless cache invalidations.
// At least, it has a huge impact on tests speed.
// Fields: `LintersSettings` and `Run.BuildTags`.
func computeConfigSalt(cfg *config.Config) ([]byte, error) {
lintersSettingsBytes, err := yaml.Marshal(cfg.LintersSettings)
if err != nil {
return nil, fmt.Errorf("failed to json marshal config linter settings: %w", err)
}
configData := bytes.NewBufferString("linters-settings=")
configData.Write(lintersSettingsBytes)
configData.WriteString("\nbuild-tags=%s" + strings.Join(cfg.Run.BuildTags, ","))
h := sha256.New()
if _, err := h.Write(configData.Bytes()); err != nil {
return nil, err
}
return h.Sum(nil), nil
}

View file

@ -5,15 +5,27 @@ import (
"os"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/exitcodes"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/logutils"
)
func (e *Executor) initConfig() {
type configCommand struct {
viper *viper.Viper
cmd *cobra.Command
log logutils.Log
}
func newConfigCommand(log logutils.Log) *configCommand {
c := &configCommand{
viper: viper.New(),
log: log,
}
configCmd := &cobra.Command{
Use: "config",
Short: "Config file information",
@ -23,25 +35,38 @@ func (e *Executor) initConfig() {
},
}
pathCmd := &cobra.Command{
Use: "path",
Short: "Print used config path",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
Run: e.executePath,
}
configCmd.AddCommand(
&cobra.Command{
Use: "path",
Short: "Print used config path",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
Run: c.execute,
PreRunE: c.preRunE,
},
)
fs := pathCmd.Flags()
fs.SortFlags = false // sort them as they are defined here
c.cmd = configCmd
configCmd.AddCommand(pathCmd)
e.rootCmd.AddCommand(configCmd)
return c
}
func (e *Executor) executePath(_ *cobra.Command, _ []string) {
usedConfigFile := e.getUsedConfig()
func (c *configCommand) preRunE(cmd *cobra.Command, _ []string) error {
// The command doesn't depend on the real configuration.
// It only needs to know the path of the configuration file.
loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), config.LoaderOptions{}, config.NewDefault())
if err := loader.Load(); err != nil {
return fmt.Errorf("can't load config: %w", err)
}
return nil
}
func (c *configCommand) execute(_ *cobra.Command, _ []string) {
usedConfigFile := c.getUsedConfig()
if usedConfigFile == "" {
e.log.Warnf("No config file detected")
c.log.Warnf("No config file detected")
os.Exit(exitcodes.NoConfigFileDetected)
}
@ -50,24 +75,17 @@ func (e *Executor) executePath(_ *cobra.Command, _ []string) {
// getUsedConfig returns the resolved path to the golangci config file,
// or the empty string if no configuration could be found.
func (e *Executor) getUsedConfig() string {
usedConfigFile := viper.ConfigFileUsed()
func (c *configCommand) getUsedConfig() string {
usedConfigFile := c.viper.ConfigFileUsed()
if usedConfigFile == "" {
return ""
}
prettyUsedConfigFile, err := fsutils.ShortestRelPath(usedConfigFile, "")
if err != nil {
e.log.Warnf("Can't pretty print config file path: %s", err)
c.log.Warnf("Can't pretty print config file path: %s", err)
return usedConfigFile
}
return prettyUsedConfigFile
}
// --- Related to config but not used directly by the config command.
func initConfigFileFlagSet(fs *pflag.FlagSet, cfg *config.Run) {
fs.StringVarP(&cfg.Config, "config", "c", "", wh("Read config from file path `PATH`"))
fs.BoolVar(&cfg.NoConfig, "no-config", false, wh("Don't read config file"))
}

View file

@ -1,230 +0,0 @@
package commands
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/fatih/color"
"github.com/gofrs/flock"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/golangci/golangci-lint/internal/pkgcache"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load"
"github.com/golangci/golangci-lint/pkg/goutil"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/report"
"github.com/golangci/golangci-lint/pkg/timeutils"
)
type Executor struct {
rootCmd *cobra.Command
runCmd *cobra.Command // used by fixSlicesFlags, printStats
lintersCmd *cobra.Command // used by fixSlicesFlags
exitCode int
buildInfo BuildInfo
cfg *config.Config // cfg is the unmarshaled data from the golangci config file.
log logutils.Log
debugf logutils.DebugFunc
reportData report.Data
dbManager *lintersdb.Manager
enabledLintersSet *lintersdb.EnabledSet
contextLoader *lint.ContextLoader
goenv *goutil.Env
fileCache *fsutils.FileCache
lineCache *fsutils.LineCache
flock *flock.Flock
}
// NewExecutor creates and initializes a new command executor.
func NewExecutor(buildInfo BuildInfo) *Executor {
e := &Executor{
cfg: config.NewDefault(),
buildInfo: buildInfo,
debugf: logutils.Debug(logutils.DebugKeyExec),
}
e.log = report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), &e.reportData)
// init of commands must be done before config file reading because init sets config with the default values of flags.
e.initCommands()
startedAt := time.Now()
e.debugf("Starting execution...")
e.initConfiguration()
e.initExecutor()
e.debugf("Initialized executor in %s", time.Since(startedAt))
return e
}
func (e *Executor) initCommands() {
e.initRoot()
e.initRun()
e.initHelp()
e.initLinters()
e.initConfig()
e.initVersion()
e.initCache()
}
func (e *Executor) initConfiguration() {
// to set up log level early we need to parse config from command line extra time to find `-v` option.
commandLineCfg, err := getConfigForCommandLine()
if err != nil && !errors.Is(err, pflag.ErrHelp) {
e.log.Fatalf("Can't get config for command line: %s", err)
}
if commandLineCfg != nil {
logutils.SetupVerboseLog(e.log, commandLineCfg.Run.IsVerbose)
switch commandLineCfg.Output.Color {
case "always":
color.NoColor = false
case "never":
color.NoColor = true
case "auto":
// nothing
default:
e.log.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", commandLineCfg.Output.Color)
}
}
// init e.cfg by values from config: flags parse will see these values like the default ones.
// It will overwrite them only if the same option is found in command-line: it's ok, command-line has higher priority.
r := config.NewFileReader(e.cfg, commandLineCfg, e.log.Child(logutils.DebugKeyConfigReader))
if err = r.Read(); err != nil {
e.log.Fatalf("Can't read config: %s", err)
}
if commandLineCfg != nil && commandLineCfg.Run.Go != "" {
// This hack allow to have the right Run information at least for the Go version (because the default value of the "go" flag is empty).
// If you put a log for `m.cfg.Run.Go` inside `GetAllSupportedLinterConfigs`,
// you will observe that at end (without this hack) the value will have the right value but too late,
// the linters are already running with the previous uncompleted configuration.
// TODO(ldez) there is a major problem with the executor:
// the parsing of the configuration and the timing to load the configuration and linters are creating unmanageable situations.
// There is no simple solution because it's spaghetti code.
// I need to completely rewrite the command line system and the executor because it's extremely time consuming to debug,
// so it's unmaintainable.
e.cfg.Run.Go = commandLineCfg.Run.Go
} else if e.cfg.Run.Go == "" {
e.cfg.Run.Go = config.DetectGoVersion()
}
// Slice options must be explicitly set for proper merging of config and command-line options.
fixSlicesFlags(e.runCmd.Flags())
fixSlicesFlags(e.lintersCmd.Flags())
}
func (e *Executor) initExecutor() {
e.dbManager = lintersdb.NewManager(e.cfg, e.log)
e.enabledLintersSet = lintersdb.NewEnabledSet(e.dbManager,
lintersdb.NewValidator(e.dbManager), e.log.Child(logutils.DebugKeyLintersDB), e.cfg)
e.goenv = goutil.NewEnv(e.log.Child(logutils.DebugKeyGoEnv))
e.fileCache = fsutils.NewFileCache()
e.lineCache = fsutils.NewLineCache(e.fileCache)
sw := timeutils.NewStopwatch("pkgcache", e.log.Child(logutils.DebugKeyStopwatch))
pkgCache, err := pkgcache.NewCache(sw, e.log.Child(logutils.DebugKeyPkgCache))
if err != nil {
e.log.Fatalf("Failed to build packages cache: %s", err)
}
e.contextLoader = lint.NewContextLoader(e.cfg, e.log.Child(logutils.DebugKeyLoader), e.goenv,
e.lineCache, e.fileCache, pkgCache, load.NewGuard())
if err = initHashSalt(e.buildInfo.Version, e.cfg); err != nil {
e.log.Fatalf("Failed to init hash salt: %s", err)
}
}
func (e *Executor) Execute() error {
return e.rootCmd.Execute()
}
func getConfigForCommandLine() (*config.Config, error) {
// We use another pflag.FlagSet here to not set `changed` flag
// on cmd.Flags() options. Otherwise, string slice options will be duplicated.
fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError)
var cfg config.Config
// Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations:
// `changed` variable inside string slice vars will be shared.
// Use another config variable here, not e.cfg, to not
// affect main parsing by this parsing of only config option.
initRunFlagSet(fs, &cfg)
initVersionFlagSet(fs, &cfg)
// Parse max options, even force version option: don't want
// to get access to Executor here: it's error-prone to use
// cfg vs e.cfg.
initRootFlagSet(fs, &cfg)
fs.Usage = func() {} // otherwise, help text will be printed twice
if err := fs.Parse(os.Args); err != nil {
if errors.Is(err, pflag.ErrHelp) {
return nil, err
}
return nil, fmt.Errorf("can't parse args: %w", err)
}
return &cfg, nil
}
func fixSlicesFlags(fs *pflag.FlagSet) {
// It's a dirty hack to set flag.Changed to true for every string slice flag.
// It's necessary to merge config and command-line slices: otherwise command-line
// flags will always overwrite ones from the config.
fs.VisitAll(func(f *pflag.Flag) {
if f.Value.Type() != "stringSlice" {
return
}
s, err := fs.GetStringSlice(f.Name)
if err != nil {
return
}
if s == nil { // assume that every string slice flag has nil as the default
return
}
var safe []string
for _, v := range s {
// add quotes to escape comma because spf13/pflag use a CSV parser:
// https://github.com/spf13/pflag/blob/85dd5c8bc61cfa382fecd072378089d4e856579d/string_slice.go#L43
safe = append(safe, `"`+v+`"`)
}
// calling Set sets Changed to true: next Set calls will append, not overwrite
_ = f.Value.Set(strings.Join(safe, ","))
})
}
func wh(text string) string {
return color.GreenString(text)
}

104
pkg/commands/flagsets.go Normal file
View file

@ -0,0 +1,104 @@
package commands
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/golangci/golangci-lint/pkg/commands/internal"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/exitcodes"
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
)
func setupLintersFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
fs.StringSliceP("disable", "D", nil, color.GreenString("Disable specific linter")) // Hack see Loader.applyStringSliceHack
internal.AddFlagAndBind(v, fs, fs.Bool, "disable-all", "linters.disable-all", false, color.GreenString("Disable all linters"))
fs.StringSliceP("enable", "E", nil, color.GreenString("Enable specific linter")) // Hack see Loader.applyStringSliceHack
internal.AddFlagAndBind(v, fs, fs.Bool, "enable-all", "linters.enable-all", false, color.GreenString("Enable all linters"))
internal.AddFlagAndBind(v, fs, fs.Bool, "fast", "linters.fast", false,
color.GreenString("Enable only fast linters from enabled linters set (first run won't be fast)"))
// Hack see Loader.applyStringSliceHack
fs.StringSliceP("presets", "p", nil,
color.GreenString(fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint help linters' to see "+
"them. This option implies option --disable-all", strings.Join(lintersdb.AllPresets(), "|"))))
}
func setupRunFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
internal.AddFlagAndBindP(v, fs, fs.IntP, "concurrency", "j", "run.concurrency", getDefaultConcurrency(),
color.GreenString("Number of CPUs to use (Default: number of logical CPUs)"))
internal.AddFlagAndBind(v, fs, fs.String, "modules-download-mode", "run.modules-download-mode", "",
color.GreenString("Modules download mode. If not empty, passed as -mod=<mode> to go tools"))
internal.AddFlagAndBind(v, fs, fs.Int, "issues-exit-code", "run.issues-exit-code", exitcodes.IssuesFound,
color.GreenString("Exit code when issues were found"))
internal.AddFlagAndBind(v, fs, fs.String, "go", "run.go", "", color.GreenString("Targeted Go version"))
fs.StringSlice("build-tags", nil, color.GreenString("Build tags")) // Hack see Loader.applyStringSliceHack
internal.AddFlagAndBind(v, fs, fs.Duration, "timeout", "run.timeout", defaultTimeout, color.GreenString("Timeout for total work"))
internal.AddFlagAndBind(v, fs, fs.Bool, "tests", "run.tests", true, color.GreenString("Analyze tests (*_test.go)"))
fs.StringSlice("skip-dirs", nil, color.GreenString("Regexps of directories to skip")) // Hack see Loader.applyStringSliceHack
internal.AddFlagAndBind(v, fs, fs.Bool, "skip-dirs-use-default", "run.skip-dirs-use-default", true, getDefaultDirectoryExcludeHelp())
fs.StringSlice("skip-files", nil, color.GreenString("Regexps of files to skip")) // Hack see Loader.applyStringSliceHack
const allowParallelDesc = "Allow multiple parallel golangci-lint instances running. " +
"If false (default) - golangci-lint acquires file lock on start."
internal.AddFlagAndBind(v, fs, fs.Bool, "allow-parallel-runners", "run.allow-parallel-runners", false,
color.GreenString(allowParallelDesc))
const allowSerialDesc = "Allow multiple golangci-lint instances running, but serialize them around a lock. " +
"If false (default) - golangci-lint exits with an error if it fails to acquire file lock on start."
internal.AddFlagAndBind(v, fs, fs.Bool, "allow-serial-runners", "run.allow-serial-runners", false, color.GreenString(allowSerialDesc))
internal.AddFlagAndBind(v, fs, fs.Bool, "show-stats", "run.show-stats", false, color.GreenString("Show statistics per linter"))
}
func setupOutputFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
internal.AddFlagAndBind(v, fs, fs.String, "out-format", "output.format", config.OutFormatColoredLineNumber,
color.GreenString(fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|"))))
internal.AddFlagAndBind(v, fs, fs.Bool, "print-issued-lines", "output.print-issued-lines", true,
color.GreenString("Print lines of code with issue"))
internal.AddFlagAndBind(v, fs, fs.Bool, "print-linter-name", "output.print-linter-name", true,
color.GreenString("Print linter name in issue line"))
internal.AddFlagAndBind(v, fs, fs.Bool, "uniq-by-line", "output.uniq-by-line", true,
color.GreenString("Make issues output unique by line"))
internal.AddFlagAndBind(v, fs, fs.Bool, "sort-results", "output.sort-results", false,
color.GreenString("Sort linter results"))
internal.AddFlagAndBind(v, fs, fs.String, "path-prefix", "output.path-prefix", "",
color.GreenString("Path prefix to add to output"))
}
//nolint:gomnd
func setupIssuesFlagSet(v *viper.Viper, fs *pflag.FlagSet) {
fs.StringSliceP("exclude", "e", nil, color.GreenString("Exclude issue by regexp")) // Hack see Loader.applyStringSliceHack
internal.AddFlagAndBind(v, fs, fs.Bool, "exclude-use-default", "issues.exclude-use-default", true,
getDefaultIssueExcludeHelp())
internal.AddFlagAndBind(v, fs, fs.Bool, "exclude-case-sensitive", "issues.exclude-case-sensitive", false,
color.GreenString("If set to true exclude and exclude rules regular expressions are case-sensitive"))
internal.AddFlagAndBind(v, fs, fs.Int, "max-issues-per-linter", "issues.max-issues-per-linter", 50,
color.GreenString("Maximum issues count per one linter. Set to 0 to disable"))
internal.AddFlagAndBind(v, fs, fs.Int, "max-same-issues", "issues.max-same-issues", 3,
color.GreenString("Maximum count of issues with the same text. Set to 0 to disable"))
const newDesc = "Show only new issues: if there are unstaged changes or untracked files, only those changes " +
"are analyzed, else only changes in HEAD~ are analyzed.\nIt's a super-useful option for integration " +
"of golangci-lint into existing large codebase.\nIt's not practical to fix all existing issues at " +
"the moment of integration: much better to not allow issues in new code.\nFor CI setups, prefer " +
"--new-from-rev=HEAD~, as --new can skip linting the current patch if any scripts generate " +
"unstaged files before golangci-lint runs."
internal.AddFlagAndBindP(v, fs, fs.BoolP, "new", "n", "issues.new", false, color.GreenString(newDesc))
internal.AddFlagAndBind(v, fs, fs.String, "new-from-rev", "issues.new-from-rev", "",
color.GreenString("Show only new issues created after git revision `REV`"))
internal.AddFlagAndBind(v, fs, fs.String, "new-from-patch", "issues.new-from-patch", "",
color.GreenString("Show only new issues created in git patch with file path `PATH`"))
internal.AddFlagAndBind(v, fs, fs.Bool, "whole-files", "issues.whole-files", false,
color.GreenString("Show issues in any part of update files (requires new-from-rev or new-from-patch)"))
internal.AddFlagAndBind(v, fs, fs.Bool, "fix", "issues.fix", false,
color.GreenString("Fix found issues (if it's supported by the linter)"))
}

View file

@ -8,12 +8,23 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
"github.com/golangci/golangci-lint/pkg/logutils"
)
func (e *Executor) initHelp() {
type helpCommand struct {
cmd *cobra.Command
dbManager *lintersdb.Manager
log logutils.Log
}
func newHelpCommand(logger logutils.Log) *helpCommand {
c := &helpCommand{log: logger}
helpCmd := &cobra.Command{
Use: "help",
Short: "Help",
@ -23,20 +34,31 @@ func (e *Executor) initHelp() {
},
}
helpCmd.AddCommand(&cobra.Command{
Use: "linters",
Short: "Help about linters",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
Run: e.executeHelp,
})
helpCmd.AddCommand(
&cobra.Command{
Use: "linters",
Short: "Help about linters",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
Run: c.execute,
PreRun: c.preRun,
},
)
e.rootCmd.SetHelpCommand(helpCmd)
c.cmd = helpCmd
return c
}
func (e *Executor) executeHelp(_ *cobra.Command, _ []string) {
func (c *helpCommand) preRun(_ *cobra.Command, _ []string) {
// The command doesn't depend on the real configuration.
// It just needs the list of all plugins and all presets.
c.dbManager = lintersdb.NewManager(config.NewDefault(), c.log)
}
func (c *helpCommand) execute(_ *cobra.Command, _ []string) {
var enabledLCs, disabledLCs []*linter.Config
for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() {
for _, lc := range c.dbManager.GetAllSupportedLinterConfigs() {
if lc.Internal {
continue
}
@ -49,13 +71,19 @@ func (e *Executor) executeHelp(_ *cobra.Command, _ []string) {
}
color.Green("Enabled by default linters:\n")
printLinterConfigs(enabledLCs)
printLinters(enabledLCs)
color.Red("\nDisabled by default linters:\n")
printLinterConfigs(disabledLCs)
printLinters(disabledLCs)
color.Green("\nLinters presets:")
c.printPresets()
}
func (c *helpCommand) printPresets() {
for _, p := range lintersdb.AllPresets() {
linters := e.dbManager.GetAllLinterConfigsForPreset(p)
linters := c.dbManager.GetAllLinterConfigsForPreset(p)
var linterNames []string
for _, lc := range linters {
if lc.Internal {
@ -65,14 +93,16 @@ func (e *Executor) executeHelp(_ *cobra.Command, _ []string) {
linterNames = append(linterNames, lc.Name())
}
sort.Strings(linterNames)
fmt.Fprintf(logutils.StdOut, "%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", "))
_, _ = fmt.Fprintf(logutils.StdOut, "%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", "))
}
}
func printLinterConfigs(lcs []*linter.Config) {
func printLinters(lcs []*linter.Config) {
sort.Slice(lcs, func(i, j int) bool {
return lcs[i].Name() < lcs[j].Name()
})
for _, lc := range lcs {
altNamesStr := ""
if len(lc.AlternativeNames) != 0 {
@ -91,7 +121,7 @@ func printLinterConfigs(lcs []*linter.Config) {
deprecatedMark = " [" + color.RedString("deprecated") + "]"
}
fmt.Fprintf(logutils.StdOut, "%s%s%s: %s [fast: %t, auto-fix: %t]\n", color.YellowString(lc.Name()),
altNamesStr, deprecatedMark, linterDescription, !lc.IsSlowLinter(), lc.CanAutoFix)
_, _ = fmt.Fprintf(logutils.StdOut, "%s%s%s: %s [fast: %t, auto-fix: %t]\n",
color.YellowString(lc.Name()), altNamesStr, deprecatedMark, linterDescription, !lc.IsSlowLinter(), lc.CanAutoFix)
}
}

View file

@ -0,0 +1,32 @@
package internal
import (
"fmt"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type FlagFunc[T any] func(name string, value T, usage string) *T
type FlagPFunc[T any] func(name, shorthand string, value T, usage string) *T
// AddFlagAndBind adds a Cobra/pflag flag and binds it with Viper.
func AddFlagAndBind[T any](v *viper.Viper, fs *pflag.FlagSet, pfn FlagFunc[T], name, bind string, value T, usage string) {
pfn(name, value, usage)
err := v.BindPFlag(bind, fs.Lookup(name))
if err != nil {
panic(fmt.Sprintf("failed to bind flag %s: %v", name, err))
}
}
// AddFlagAndBindP adds a Cobra/pflag flag and binds it with Viper.
func AddFlagAndBindP[T any](v *viper.Viper, fs *pflag.FlagSet, pfn FlagPFunc[T], name, shorthand, bind string, value T, usage string) {
pfn(name, shorthand, value, usage)
err := v.BindPFlag(bind, fs.Lookup(name))
if err != nil {
panic(fmt.Sprintf("failed to bind flag %s: %v", name, err))
}
}

View file

@ -2,40 +2,78 @@ package commands
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
"github.com/golangci/golangci-lint/pkg/logutils"
)
func (e *Executor) initLinters() {
type lintersOptions struct {
config.LoaderOptions
}
type lintersCommand struct {
viper *viper.Viper
cmd *cobra.Command
opts lintersOptions
cfg *config.Config
log logutils.Log
dbManager *lintersdb.Manager
enabledLintersSet *lintersdb.EnabledSet
}
func newLintersCommand(logger logutils.Log, cfg *config.Config) *lintersCommand {
c := &lintersCommand{
viper: viper.New(),
cfg: cfg,
log: logger,
}
lintersCmd := &cobra.Command{
Use: "linters",
Short: "List current linters configuration",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: e.executeLinters,
RunE: c.execute,
PreRunE: c.preRunE,
}
fs := lintersCmd.Flags()
fs.SortFlags = false // sort them as they are defined here
initConfigFileFlagSet(fs, &e.cfg.Run)
initLintersFlagSet(fs, &e.cfg.Linters)
setupConfigFileFlagSet(fs, &c.opts.LoaderOptions)
setupLintersFlagSet(c.viper, fs)
e.rootCmd.AddCommand(lintersCmd)
c.cmd = lintersCmd
e.lintersCmd = lintersCmd
return c
}
// executeLinters runs the 'linters' CLI command, which displays the supported linters.
func (e *Executor) executeLinters(_ *cobra.Command, _ []string) error {
enabledLintersMap, err := e.enabledLintersSet.GetEnabledLintersMap()
func (c *lintersCommand) preRunE(cmd *cobra.Command, _ []string) error {
loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), c.opts.LoaderOptions, c.cfg)
if err := loader.Load(); err != nil {
return fmt.Errorf("can't load config: %w", err)
}
c.dbManager = lintersdb.NewManager(c.cfg, c.log)
c.enabledLintersSet = lintersdb.NewEnabledSet(c.dbManager,
lintersdb.NewValidator(c.dbManager), c.log.Child(logutils.DebugKeyLintersDB), c.cfg)
return nil
}
func (c *lintersCommand) execute(_ *cobra.Command, _ []string) error {
enabledLintersMap, err := c.enabledLintersSet.GetEnabledLintersMap()
if err != nil {
return fmt.Errorf("can't get enabled linters: %w", err)
}
@ -43,7 +81,7 @@ func (e *Executor) executeLinters(_ *cobra.Command, _ []string) error {
var enabledLinters []*linter.Config
var disabledLCs []*linter.Config
for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() {
for _, lc := range c.dbManager.GetAllSupportedLinterConfigs() {
if lc.Internal {
continue
}
@ -56,20 +94,9 @@ func (e *Executor) executeLinters(_ *cobra.Command, _ []string) error {
}
color.Green("Enabled by your configuration linters:\n")
printLinterConfigs(enabledLinters)
printLinters(enabledLinters)
color.Red("\nDisabled by your configuration linters:\n")
printLinterConfigs(disabledLCs)
printLinters(disabledLCs)
return nil
}
func initLintersFlagSet(fs *pflag.FlagSet, cfg *config.Linters) {
fs.StringSliceVarP(&cfg.Disable, "disable", "D", nil, wh("Disable specific linter"))
fs.BoolVar(&cfg.DisableAll, "disable-all", false, wh("Disable all linters"))
fs.StringSliceVarP(&cfg.Enable, "enable", "E", nil, wh("Enable specific linter"))
fs.BoolVar(&cfg.EnableAll, "enable-all", false, wh("Enable all linters"))
fs.BoolVar(&cfg.Fast, "fast", false, wh("Enable only fast linters from enabled linters set (first run won't be fast)"))
fs.StringSliceVarP(&cfg.Presets, "presets", "p", nil,
wh(fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint help linters' to see "+
"them. This option implies option --disable-all", strings.Join(lintersdb.AllPresets(), "|"))))
}

View file

@ -1,160 +1,168 @@
package commands
import (
"errors"
"fmt"
"os"
"runtime"
"runtime/pprof"
"runtime/trace"
"strconv"
"slices"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/exitcodes"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/report"
)
const (
// envHelpRun value: "1".
envHelpRun = "HELP_RUN"
envMemProfileRate = "GL_MEM_PROFILE_RATE"
)
func Execute(info BuildInfo) error {
return newRootCommand(info).Execute()
}
type rootOptions struct {
PrintVersion bool // Flag only.
Verbose bool // Flag only.
Color string // Flag only.
}
type rootCommand struct {
cmd *cobra.Command
opts rootOptions
log logutils.Log
}
func newRootCommand(info BuildInfo) *rootCommand {
c := &rootCommand{}
func (e *Executor) initRoot() {
rootCmd := &cobra.Command{
Use: "golangci-lint",
Short: "golangci-lint is a smart linters runner.",
Long: `Smart, fast linters runner.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
if c.opts.PrintVersion {
_ = printVersion(logutils.StdOut, info)
return nil
}
return cmd.Help()
},
PersistentPreRunE: e.persistentPreRun,
PersistentPostRunE: e.persistentPostRun,
}
initRootFlagSet(rootCmd.PersistentFlags(), e.cfg)
fs := rootCmd.Flags()
fs.BoolVar(&c.opts.PrintVersion, "version", false, color.GreenString("Print version"))
e.rootCmd = rootCmd
setupRootPersistentFlags(rootCmd.PersistentFlags(), &c.opts)
reportData := &report.Data{}
log := report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), reportData)
// Dedicated configuration for each command to avoid side effects of bindings.
rootCmd.AddCommand(
newLintersCommand(log, config.NewDefault()).cmd,
newRunCommand(log, config.NewDefault(), reportData, info).cmd,
newCacheCommand().cmd,
newConfigCommand(log).cmd,
newVersionCommand(info).cmd,
)
rootCmd.SetHelpCommand(newHelpCommand(log).cmd)
c.log = log
c.cmd = rootCmd
return c
}
func (e *Executor) persistentPreRun(_ *cobra.Command, _ []string) error {
if e.cfg.Run.PrintVersion {
_ = printVersion(logutils.StdOut, e.buildInfo)
os.Exit(exitcodes.Success) // a return nil is not enough to stop the process because we are inside the `preRun`.
func (c *rootCommand) Execute() error {
err := setupLogger(c.log)
if err != nil {
return err
}
runtime.GOMAXPROCS(e.cfg.Run.Concurrency)
return c.cmd.Execute()
}
if e.cfg.Run.CPUProfilePath != "" {
f, err := os.Create(e.cfg.Run.CPUProfilePath)
if err != nil {
return fmt.Errorf("can't create file %s: %w", e.cfg.Run.CPUProfilePath, err)
}
if err := pprof.StartCPUProfile(f); err != nil {
return fmt.Errorf("can't start CPU profiling: %w", err)
}
func setupRootPersistentFlags(fs *pflag.FlagSet, opts *rootOptions) {
fs.BoolVarP(&opts.Verbose, "verbose", "v", false, color.GreenString("Verbose output"))
fs.StringVar(&opts.Color, "color", "auto", color.GreenString("Use color when printing; can be 'always', 'auto', or 'never'"))
}
func setupLogger(logger logutils.Log) error {
opts, err := forceRootParsePersistentFlags()
if err != nil && !errors.Is(err, pflag.ErrHelp) {
return err
}
if e.cfg.Run.MemProfilePath != "" {
if rate := os.Getenv(envMemProfileRate); rate != "" {
runtime.MemProfileRate, _ = strconv.Atoi(rate)
}
if opts == nil {
return nil
}
if e.cfg.Run.TracePath != "" {
f, err := os.Create(e.cfg.Run.TracePath)
if err != nil {
return fmt.Errorf("can't create file %s: %w", e.cfg.Run.TracePath, err)
}
if err = trace.Start(f); err != nil {
return fmt.Errorf("can't start tracing: %w", err)
}
logutils.SetupVerboseLog(logger, opts.Verbose)
switch opts.Color {
case "always":
color.NoColor = false
case "never":
color.NoColor = true
case "auto":
// nothing
default:
logger.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", opts.Color)
}
return nil
}
func (e *Executor) persistentPostRun(_ *cobra.Command, _ []string) error {
if e.cfg.Run.CPUProfilePath != "" {
pprof.StopCPUProfile()
}
func forceRootParsePersistentFlags() (*rootOptions, error) {
// We use another pflag.FlagSet here to not set `changed` flag on cmd.Flags() options.
// Otherwise, string slice options will be duplicated.
fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError)
if e.cfg.Run.MemProfilePath != "" {
f, err := os.Create(e.cfg.Run.MemProfilePath)
if err != nil {
return fmt.Errorf("can't create file %s: %w", e.cfg.Run.MemProfilePath, err)
// Ignore unknown flags because we will parse the command flags later.
fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
opts := &rootOptions{}
// Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations:
// `changed` variable inside string slice vars will be shared.
// Use another config variable here,
// to not affect main parsing by this parsing of only config option.
setupRootPersistentFlags(fs, opts)
fs.Usage = func() {} // otherwise, help text will be printed twice
if err := fs.Parse(safeArgs(fs, os.Args)); err != nil {
if errors.Is(err, pflag.ErrHelp) {
return nil, err
}
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
printMemStats(&ms, e.log)
return nil, fmt.Errorf("can't parse args: %w", err)
}
if err := pprof.WriteHeapProfile(f); err != nil {
return fmt.Errorf("can't write heap profile: %w", err)
return opts, nil
}
// Shorthands are a problem because pflag, with UnknownFlags, will try to parse all the letters as options.
// A shorthand can aggregate several letters (ex `ps -aux`)
// The function replaces non-supported shorthands by a dumb flag.
func safeArgs(fs *pflag.FlagSet, args []string) []string {
var shorthands []string
fs.VisitAll(func(flag *pflag.Flag) {
shorthands = append(shorthands, flag.Shorthand)
})
var cleanArgs []string
for _, arg := range args {
if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' && !slices.Contains(shorthands, string(arg[1])) {
cleanArgs = append(cleanArgs, "--potato")
continue
}
_ = f.Close()
cleanArgs = append(cleanArgs, arg)
}
if e.cfg.Run.TracePath != "" {
trace.Stop()
}
os.Exit(e.exitCode)
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 "+
"stack_in_use=%s stack_sys=%s "+
"mspan_sys=%s mcache_sys=%s buck_hash_sys=%s gc_sys=%s other_sys=%s "+
"mallocs_n=%d frees_n=%d heap_objects_n=%d gc_cpu_fraction=%.2f",
formatMemory(ms.Alloc), formatMemory(ms.TotalAlloc), formatMemory(ms.Sys),
formatMemory(ms.HeapAlloc), formatMemory(ms.HeapSys),
formatMemory(ms.HeapIdle), formatMemory(ms.HeapReleased), formatMemory(ms.HeapInuse),
formatMemory(ms.StackInuse), formatMemory(ms.StackSys),
formatMemory(ms.MSpanSys), formatMemory(ms.MCacheSys), formatMemory(ms.BuckHashSys),
formatMemory(ms.GCSys), formatMemory(ms.OtherSys),
ms.Mallocs, ms.Frees, ms.HeapObjects, ms.GCCPUFraction)
}
func formatMemory(memBytes uint64) string {
const Kb = 1024
const Mb = Kb * 1024
if memBytes < Kb {
return fmt.Sprintf("%db", memBytes)
}
if memBytes < Mb {
return fmt.Sprintf("%dkb", memBytes/Kb)
}
return fmt.Sprintf("%dmb", memBytes/Mb)
}
func getDefaultConcurrency() int {
if os.Getenv(envHelpRun) == "1" {
// Make stable concurrency for generating help documentation.
const prettyConcurrency = 8
return prettyConcurrency
}
return runtime.NumCPU()
return cleanArgs
}

View file

@ -1,7 +1,9 @@
package commands
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
@ -9,7 +11,10 @@ import (
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"runtime/trace"
"sort"
"strconv"
"strings"
"time"
@ -17,16 +22,25 @@ import (
"github.com/gofrs/flock"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"golang.org/x/exp/maps"
"gopkg.in/yaml.v3"
"github.com/golangci/golangci-lint/internal/cache"
"github.com/golangci/golangci-lint/internal/pkgcache"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/exitcodes"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load"
"github.com/golangci/golangci-lint/pkg/goutil"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/packages"
"github.com/golangci/golangci-lint/pkg/printers"
"github.com/golangci/golangci-lint/pkg/report"
"github.com/golangci/golangci-lint/pkg/result"
"github.com/golangci/golangci-lint/pkg/timeutils"
)
const defaultFileMode = 0644
@ -40,20 +54,68 @@ const (
envMemLogEvery = "GL_MEM_LOG_EVERY"
)
func (e *Executor) initRun() {
const (
// envHelpRun value: "1".
envHelpRun = "HELP_RUN"
envMemProfileRate = "GL_MEM_PROFILE_RATE"
)
type runOptions struct {
config.LoaderOptions
CPUProfilePath string // Flag only.
MemProfilePath string // Flag only.
TracePath string // Flag only.
PrintResourcesUsage bool // Flag only.
}
type runCommand struct {
viper *viper.Viper
cmd *cobra.Command
opts runOptions
cfg *config.Config
buildInfo BuildInfo
dbManager *lintersdb.Manager
enabledLintersSet *lintersdb.EnabledSet
log logutils.Log
debugf logutils.DebugFunc
reportData *report.Data
contextLoader *lint.ContextLoader
goenv *goutil.Env
fileCache *fsutils.FileCache
lineCache *fsutils.LineCache
flock *flock.Flock
exitCode int
}
func newRunCommand(logger logutils.Log, cfg *config.Config, reportData *report.Data, info BuildInfo) *runCommand {
c := &runCommand{
viper: viper.New(),
log: logger,
debugf: logutils.Debug(logutils.DebugKeyExec),
cfg: cfg,
reportData: reportData,
buildInfo: info,
}
runCmd := &cobra.Command{
Use: "run",
Short: "Run the linters",
Run: e.executeRun,
PreRunE: func(_ *cobra.Command, _ []string) error {
if ok := e.acquireFileLock(); !ok {
return errors.New("parallel golangci-lint is running")
}
return nil
},
PostRun: func(_ *cobra.Command, _ []string) {
e.releaseFileLock()
},
Use: "run",
Short: "Run the linters",
Run: c.execute,
PreRunE: c.preRunE,
PostRun: c.postRun,
PersistentPreRunE: c.persistentPreRunE,
PersistentPostRunE: c.persistentPostRunE,
}
runCmd.SetOut(logutils.StdOut) // use custom output to properly color it in Windows terminals
@ -62,16 +124,90 @@ func (e *Executor) initRun() {
fs := runCmd.Flags()
fs.SortFlags = false // sort them as they are defined here
initRunFlagSet(fs, e.cfg)
// Only for testing purpose.
// Don't add other flags here.
fs.BoolVar(&cfg.InternalCmdTest, "internal-cmd-test", false,
color.GreenString("Option is used only for testing golangci-lint command, don't use it"))
_ = fs.MarkHidden("internal-cmd-test")
e.rootCmd.AddCommand(runCmd)
setupConfigFileFlagSet(fs, &c.opts.LoaderOptions)
e.runCmd = runCmd
setupLintersFlagSet(c.viper, fs)
setupRunFlagSet(c.viper, fs)
setupOutputFlagSet(c.viper, fs)
setupIssuesFlagSet(c.viper, fs)
setupRunPersistentFlags(runCmd.PersistentFlags(), &c.opts)
c.cmd = runCmd
return c
}
// executeRun executes the 'run' CLI command, which runs the linters.
func (e *Executor) executeRun(_ *cobra.Command, args []string) {
needTrackResources := e.cfg.Run.IsVerbose || e.cfg.Run.PrintResourcesUsage
func (c *runCommand) persistentPreRunE(cmd *cobra.Command, _ []string) error {
if err := c.startTracing(); err != nil {
return err
}
loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), c.opts.LoaderOptions, c.cfg)
if err := loader.Load(); err != nil {
return fmt.Errorf("can't load config: %w", err)
}
runtime.GOMAXPROCS(c.cfg.Run.Concurrency)
return c.startTracing()
}
func (c *runCommand) persistentPostRunE(_ *cobra.Command, _ []string) error {
if err := c.stopTracing(); err != nil {
return err
}
os.Exit(c.exitCode)
return nil
}
func (c *runCommand) preRunE(_ *cobra.Command, _ []string) error {
c.dbManager = lintersdb.NewManager(c.cfg, c.log)
c.enabledLintersSet = lintersdb.NewEnabledSet(c.dbManager,
lintersdb.NewValidator(c.dbManager), c.log.Child(logutils.DebugKeyLintersDB), c.cfg)
c.goenv = goutil.NewEnv(c.log.Child(logutils.DebugKeyGoEnv))
c.fileCache = fsutils.NewFileCache()
c.lineCache = fsutils.NewLineCache(c.fileCache)
sw := timeutils.NewStopwatch("pkgcache", c.log.Child(logutils.DebugKeyStopwatch))
pkgCache, err := pkgcache.NewCache(sw, c.log.Child(logutils.DebugKeyPkgCache))
if err != nil {
return fmt.Errorf("failed to build packages cache: %w", err)
}
c.contextLoader = lint.NewContextLoader(c.cfg, c.log.Child(logutils.DebugKeyLoader), c.goenv,
c.lineCache, c.fileCache, pkgCache, load.NewGuard())
if err = initHashSalt(c.buildInfo.Version, c.cfg); err != nil {
return fmt.Errorf("failed to init hash salt: %w", err)
}
if ok := c.acquireFileLock(); !ok {
return errors.New("parallel golangci-lint is running")
}
return nil
}
func (c *runCommand) postRun(_ *cobra.Command, _ []string) {
c.releaseFileLock()
}
func (c *runCommand) execute(_ *cobra.Command, args []string) {
needTrackResources := logutils.IsVerbose() || c.opts.PrintResourcesUsage
trackResourcesEndCh := make(chan struct{})
defer func() { // XXX: this defer must be before ctx.cancel defer
if needTrackResources { // wait until resource tracking finished to print properly
@ -79,96 +215,154 @@ func (e *Executor) executeRun(_ *cobra.Command, args []string) {
}
}()
ctx, cancel := context.WithTimeout(context.Background(), e.cfg.Run.Timeout)
ctx, cancel := context.WithTimeout(context.Background(), c.cfg.Run.Timeout)
defer cancel()
if needTrackResources {
go watchResources(ctx, trackResourcesEndCh, e.log, e.debugf)
go watchResources(ctx, trackResourcesEndCh, c.log, c.debugf)
}
if err := e.runAndPrint(ctx, args); err != nil {
e.log.Errorf("Running error: %s", err)
if e.exitCode == exitcodes.Success {
if err := c.runAndPrint(ctx, args); err != nil {
c.log.Errorf("Running error: %s", err)
if c.exitCode == exitcodes.Success {
var exitErr *exitcodes.ExitError
if errors.As(err, &exitErr) {
e.exitCode = exitErr.Code
c.exitCode = exitErr.Code
} else {
e.exitCode = exitcodes.Failure
c.exitCode = exitcodes.Failure
}
}
}
e.setupExitCode(ctx)
c.setupExitCode(ctx)
}
func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
if err := e.goenv.Discover(ctx); err != nil {
e.log.Warnf("Failed to discover go env: %s", err)
func (c *runCommand) startTracing() error {
if c.opts.CPUProfilePath != "" {
f, err := os.Create(c.opts.CPUProfilePath)
if err != nil {
return fmt.Errorf("can't create file %s: %w", c.opts.CPUProfilePath, err)
}
if err := pprof.StartCPUProfile(f); err != nil {
return fmt.Errorf("can't start CPU profiling: %w", err)
}
}
if c.opts.MemProfilePath != "" {
if rate := os.Getenv(envMemProfileRate); rate != "" {
runtime.MemProfileRate, _ = strconv.Atoi(rate)
}
}
if c.opts.TracePath != "" {
f, err := os.Create(c.opts.TracePath)
if err != nil {
return fmt.Errorf("can't create file %s: %w", c.opts.TracePath, err)
}
if err = trace.Start(f); err != nil {
return fmt.Errorf("can't start tracing: %w", err)
}
}
return nil
}
func (c *runCommand) stopTracing() error {
if c.opts.CPUProfilePath != "" {
pprof.StopCPUProfile()
}
if c.opts.MemProfilePath != "" {
f, err := os.Create(c.opts.MemProfilePath)
if err != nil {
return fmt.Errorf("can't create file %s: %w", c.opts.MemProfilePath, err)
}
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
printMemStats(&ms, c.log)
if err := pprof.WriteHeapProfile(f); err != nil {
return fmt.Errorf("can't write heap profile: %w", err)
}
_ = f.Close()
}
if c.opts.TracePath != "" {
trace.Stop()
}
return nil
}
func (c *runCommand) runAndPrint(ctx context.Context, args []string) error {
if err := c.goenv.Discover(ctx); err != nil {
c.log.Warnf("Failed to discover go env: %s", err)
}
if !logutils.HaveDebugTag(logutils.DebugKeyLintersOutput) {
// Don't allow linters and loader to print anything
log.SetOutput(io.Discard)
savedStdout, savedStderr := e.setOutputToDevNull()
savedStdout, savedStderr := c.setOutputToDevNull()
defer func() {
os.Stdout, os.Stderr = savedStdout, savedStderr
}()
}
issues, err := e.runAnalysis(ctx, args)
issues, err := c.runAnalysis(ctx, args)
if err != nil {
return err // XXX: don't loose type
}
formats := strings.Split(e.cfg.Output.Format, ",")
formats := strings.Split(c.cfg.Output.Format, ",")
for _, format := range formats {
out := strings.SplitN(format, ":", 2)
if len(out) < 2 {
out = append(out, "")
}
err := e.printReports(issues, out[1], out[0])
err := c.printReports(issues, out[1], out[0])
if err != nil {
return err
}
}
e.printStats(issues)
c.printStats(issues)
e.setExitCodeIfIssuesFound(issues)
c.setExitCodeIfIssuesFound(issues)
e.fileCache.PrintStats(e.log)
c.fileCache.PrintStats(c.log)
return nil
}
// runAnalysis executes the linters that have been enabled in the configuration.
func (e *Executor) runAnalysis(ctx context.Context, args []string) ([]result.Issue, error) {
e.cfg.Run.Args = args
func (c *runCommand) runAnalysis(ctx context.Context, args []string) ([]result.Issue, error) {
c.cfg.Run.Args = args
lintersToRun, err := e.enabledLintersSet.GetOptimizedLinters()
lintersToRun, err := c.enabledLintersSet.GetOptimizedLinters()
if err != nil {
return nil, err
}
enabledLintersMap, err := e.enabledLintersSet.GetEnabledLintersMap()
enabledLintersMap, err := c.enabledLintersSet.GetEnabledLintersMap()
if err != nil {
return nil, err
}
for _, lc := range e.dbManager.GetAllSupportedLinterConfigs() {
for _, lc := range c.dbManager.GetAllSupportedLinterConfigs() {
isEnabled := enabledLintersMap[lc.Name()] != nil
e.reportData.AddLinter(lc.Name(), isEnabled, lc.EnabledByDefault)
c.reportData.AddLinter(lc.Name(), isEnabled, lc.EnabledByDefault)
}
lintCtx, err := e.contextLoader.Load(ctx, lintersToRun)
lintCtx, err := c.contextLoader.Load(ctx, lintersToRun)
if err != nil {
return nil, fmt.Errorf("context loading failed: %w", err)
}
lintCtx.Log = e.log.Child(logutils.DebugKeyLintersContext)
lintCtx.Log = c.log.Child(logutils.DebugKeyLintersContext)
runner, err := lint.NewRunner(e.cfg, e.log.Child(logutils.DebugKeyRunner),
e.goenv, e.enabledLintersSet, e.lineCache, e.fileCache, e.dbManager, lintCtx.Packages)
runner, err := lint.NewRunner(c.cfg, c.log.Child(logutils.DebugKeyRunner),
c.goenv, c.enabledLintersSet, c.lineCache, c.fileCache, c.dbManager, lintCtx.Packages)
if err != nil {
return nil, err
}
@ -176,11 +370,11 @@ func (e *Executor) runAnalysis(ctx context.Context, args []string) ([]result.Iss
return runner.Run(ctx, lintersToRun, lintCtx)
}
func (e *Executor) setOutputToDevNull() (savedStdout, savedStderr *os.File) {
func (c *runCommand) setOutputToDevNull() (savedStdout, savedStderr *os.File) {
savedStdout, savedStderr = os.Stdout, os.Stderr
devNull, err := os.Open(os.DevNull)
if err != nil {
e.log.Warnf("Can't open null device %q: %s", os.DevNull, err)
c.log.Warnf("Can't open null device %q: %s", os.DevNull, err)
return
}
@ -188,19 +382,19 @@ func (e *Executor) setOutputToDevNull() (savedStdout, savedStderr *os.File) {
return
}
func (e *Executor) setExitCodeIfIssuesFound(issues []result.Issue) {
func (c *runCommand) setExitCodeIfIssuesFound(issues []result.Issue) {
if len(issues) != 0 {
e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound
c.exitCode = c.cfg.Run.ExitCodeIfIssuesFound
}
}
func (e *Executor) printReports(issues []result.Issue, path, format string) error {
w, shouldClose, err := e.createWriter(path)
func (c *runCommand) printReports(issues []result.Issue, path, format string) error {
w, shouldClose, err := c.createWriter(path)
if err != nil {
return fmt.Errorf("can't create output for %s: %w", path, err)
}
p, err := e.createPrinter(format, w)
p, err := c.createPrinter(format, w)
if err != nil {
if file, ok := w.(io.Closer); shouldClose && ok {
_ = file.Close()
@ -222,7 +416,7 @@ func (e *Executor) printReports(issues []result.Issue, path, format string) erro
return nil
}
func (e *Executor) createWriter(path string) (io.Writer, bool, error) {
func (c *runCommand) createWriter(path string) (io.Writer, bool, error) {
if path == "" || path == "stdout" {
return logutils.StdOut, false, nil
}
@ -236,19 +430,19 @@ func (e *Executor) createWriter(path string) (io.Writer, bool, error) {
return f, true, nil
}
func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer, error) {
func (c *runCommand) createPrinter(format string, w io.Writer) (printers.Printer, error) {
var p printers.Printer
switch format {
case config.OutFormatJSON:
p = printers.NewJSON(&e.reportData, w)
p = printers.NewJSON(c.reportData, w)
case config.OutFormatColoredLineNumber, config.OutFormatLineNumber:
p = printers.NewText(e.cfg.Output.PrintIssuedLine,
format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName,
e.log.Child(logutils.DebugKeyTextPrinter), w)
p = printers.NewText(c.cfg.Output.PrintIssuedLine,
format == config.OutFormatColoredLineNumber, c.cfg.Output.PrintLinterName,
c.log.Child(logutils.DebugKeyTextPrinter), w)
case config.OutFormatTab, config.OutFormatColoredTab:
p = printers.NewTab(e.cfg.Output.PrintLinterName,
p = printers.NewTab(c.cfg.Output.PrintLinterName,
format == config.OutFormatColoredTab,
e.log.Child(logutils.DebugKeyTabPrinter), w)
c.log.Child(logutils.DebugKeyTabPrinter), w)
case config.OutFormatCheckstyle:
p = printers.NewCheckstyle(w)
case config.OutFormatCodeClimate:
@ -268,13 +462,13 @@ func (e *Executor) createPrinter(format string, w io.Writer) (printers.Printer,
return p, nil
}
func (e *Executor) printStats(issues []result.Issue) {
if !e.cfg.Run.ShowStats {
func (c *runCommand) printStats(issues []result.Issue) {
if !c.cfg.Run.ShowStats {
return
}
if len(issues) == 0 {
e.runCmd.Println("0 issues.")
c.cmd.Println("0 issues.")
return
}
@ -283,53 +477,53 @@ func (e *Executor) printStats(issues []result.Issue) {
stats[issues[idx].FromLinter]++
}
e.runCmd.Printf("%d issues:\n", len(issues))
c.cmd.Printf("%d issues:\n", len(issues))
keys := maps.Keys(stats)
sort.Strings(keys)
for _, key := range keys {
e.runCmd.Printf("* %s: %d\n", key, stats[key])
c.cmd.Printf("* %s: %d\n", key, stats[key])
}
}
func (e *Executor) setupExitCode(ctx context.Context) {
func (c *runCommand) setupExitCode(ctx context.Context) {
if ctx.Err() != nil {
e.exitCode = exitcodes.Timeout
e.log.Errorf("Timeout exceeded: try increasing it by passing --timeout option")
c.exitCode = exitcodes.Timeout
c.log.Errorf("Timeout exceeded: try increasing it by passing --timeout option")
return
}
if e.exitCode != exitcodes.Success {
if c.exitCode != exitcodes.Success {
return
}
needFailOnWarnings := os.Getenv(lintersdb.EnvTestRun) == "1" || os.Getenv(envFailOnWarnings) == "1"
if needFailOnWarnings && len(e.reportData.Warnings) != 0 {
e.exitCode = exitcodes.WarningInTest
if needFailOnWarnings && len(c.reportData.Warnings) != 0 {
c.exitCode = exitcodes.WarningInTest
return
}
if e.reportData.Error != "" {
if c.reportData.Error != "" {
// it's a case e.g. when typecheck linter couldn't parse and error and just logged it
e.exitCode = exitcodes.ErrorWasLogged
c.exitCode = exitcodes.ErrorWasLogged
return
}
}
func (e *Executor) acquireFileLock() bool {
if e.cfg.Run.AllowParallelRunners {
e.debugf("Parallel runners are allowed, no locking")
func (c *runCommand) acquireFileLock() bool {
if c.cfg.Run.AllowParallelRunners {
c.debugf("Parallel runners are allowed, no locking")
return true
}
lockFile := filepath.Join(os.TempDir(), "golangci-lint.lock")
e.debugf("Locking on file %s...", lockFile)
c.debugf("Locking on file %s...", lockFile)
f := flock.New(lockFile)
const retryDelay = time.Second
ctx := context.Background()
if !e.cfg.Run.AllowSerialRunners {
if !c.cfg.Run.AllowSerialRunners {
const totalTimeout = 5 * time.Second
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, totalTimeout)
@ -339,108 +533,23 @@ func (e *Executor) acquireFileLock() bool {
return false
}
e.flock = f
c.flock = f
return true
}
func (e *Executor) releaseFileLock() {
if e.cfg.Run.AllowParallelRunners {
func (c *runCommand) releaseFileLock() {
if c.cfg.Run.AllowParallelRunners {
return
}
if err := e.flock.Unlock(); err != nil {
e.debugf("Failed to unlock on file: %s", err)
if err := c.flock.Unlock(); err != nil {
c.debugf("Failed to unlock on file: %s", err)
}
if err := os.Remove(e.flock.Path()); err != nil {
e.debugf("Failed to remove lock file: %s", err)
if err := os.Remove(c.flock.Path()); err != nil {
c.debugf("Failed to remove lock file: %s", err)
}
}
//nolint:gomnd
func initRunFlagSet(fs *pflag.FlagSet, cfg *config.Config) {
fs.BoolVar(&cfg.InternalCmdTest, "internal-cmd-test", false, wh("Option is used only for testing golangci-lint command, don't use it"))
if err := fs.MarkHidden("internal-cmd-test"); err != nil {
panic(err)
}
// --- Output config
oc := &cfg.Output
fs.StringVar(&oc.Format, "out-format",
config.OutFormatColoredLineNumber,
wh(fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|"))))
fs.BoolVar(&oc.PrintIssuedLine, "print-issued-lines", true, wh("Print lines of code with issue"))
fs.BoolVar(&oc.PrintLinterName, "print-linter-name", true, wh("Print linter name in issue line"))
fs.BoolVar(&oc.UniqByLine, "uniq-by-line", true, wh("Make issues output unique by line"))
fs.BoolVar(&oc.SortResults, "sort-results", false, wh("Sort linter results"))
fs.BoolVar(&oc.PrintWelcomeMessage, "print-welcome", false, wh("Print welcome message"))
fs.StringVar(&oc.PathPrefix, "path-prefix", "", wh("Path prefix to add to output"))
// --- Run config
rc := &cfg.Run
// Config file config
initConfigFileFlagSet(fs, rc)
fs.StringVar(&rc.ModulesDownloadMode, "modules-download-mode", "",
wh("Modules download mode. If not empty, passed as -mod=<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")
@ -497,6 +606,11 @@ func watchResources(ctx context.Context, done chan struct{}, logger logutils.Log
close(done)
}
func setupConfigFileFlagSet(fs *pflag.FlagSet, cfg *config.LoaderOptions) {
fs.StringVarP(&cfg.Config, "config", "c", "", color.GreenString("Read config from file path `PATH`"))
fs.BoolVar(&cfg.NoConfig, "no-config", false, color.GreenString("Don't read config file"))
}
func getDefaultIssueExcludeHelp() string {
parts := []string{color.GreenString("Use or not use default excludes:")}
for _, ep := range config.DefaultExcludePatterns {
@ -517,3 +631,115 @@ func getDefaultDirectoryExcludeHelp() string {
parts = append(parts, "")
return strings.Join(parts, "\n")
}
func setupRunPersistentFlags(fs *pflag.FlagSet, opts *runOptions) {
fs.BoolVar(&opts.PrintResourcesUsage, "print-resources-usage", false,
color.GreenString("Print avg and max memory usage of golangci-lint and total time"))
fs.StringVar(&opts.CPUProfilePath, "cpu-profile-path", "", color.GreenString("Path to CPU profile output file"))
fs.StringVar(&opts.MemProfilePath, "mem-profile-path", "", color.GreenString("Path to memory profile output file"))
fs.StringVar(&opts.TracePath, "trace-path", "", color.GreenString("Path to trace output file"))
}
func getDefaultConcurrency() int {
if os.Getenv(envHelpRun) == "1" {
// Make stable concurrency for generating help documentation.
const prettyConcurrency = 8
return prettyConcurrency
}
return runtime.NumCPU()
}
func printMemStats(ms *runtime.MemStats, logger logutils.Log) {
logger.Infof("Mem stats: alloc=%s total_alloc=%s sys=%s "+
"heap_alloc=%s heap_sys=%s heap_idle=%s heap_released=%s heap_in_use=%s "+
"stack_in_use=%s stack_sys=%s "+
"mspan_sys=%s mcache_sys=%s buck_hash_sys=%s gc_sys=%s other_sys=%s "+
"mallocs_n=%d frees_n=%d heap_objects_n=%d gc_cpu_fraction=%.2f",
formatMemory(ms.Alloc), formatMemory(ms.TotalAlloc), formatMemory(ms.Sys),
formatMemory(ms.HeapAlloc), formatMemory(ms.HeapSys),
formatMemory(ms.HeapIdle), formatMemory(ms.HeapReleased), formatMemory(ms.HeapInuse),
formatMemory(ms.StackInuse), formatMemory(ms.StackSys),
formatMemory(ms.MSpanSys), formatMemory(ms.MCacheSys), formatMemory(ms.BuckHashSys),
formatMemory(ms.GCSys), formatMemory(ms.OtherSys),
ms.Mallocs, ms.Frees, ms.HeapObjects, ms.GCCPUFraction)
}
func formatMemory(memBytes uint64) string {
const Kb = 1024
const Mb = Kb * 1024
if memBytes < Kb {
return fmt.Sprintf("%db", memBytes)
}
if memBytes < Mb {
return fmt.Sprintf("%dkb", memBytes/Kb)
}
return fmt.Sprintf("%dmb", memBytes/Mb)
}
// --- Related to cache.
func initHashSalt(version string, cfg *config.Config) error {
binSalt, err := computeBinarySalt(version)
if err != nil {
return fmt.Errorf("failed to calculate binary salt: %w", err)
}
configSalt, err := computeConfigSalt(cfg)
if err != nil {
return fmt.Errorf("failed to calculate config salt: %w", err)
}
b := bytes.NewBuffer(binSalt)
b.Write(configSalt)
cache.SetSalt(b.Bytes())
return nil
}
func computeBinarySalt(version string) ([]byte, error) {
if version != "" && version != "(devel)" {
return []byte(version), nil
}
if logutils.HaveDebugTag(logutils.DebugKeyBinSalt) {
return []byte("debug"), nil
}
p, err := os.Executable()
if err != nil {
return nil, err
}
f, err := os.Open(p)
if err != nil {
return nil, err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// computeConfigSalt computes configuration hash.
// We don't hash all config fields to reduce meaningless cache invalidations.
// At least, it has a huge impact on tests speed.
// Fields: `LintersSettings` and `Run.BuildTags`.
func computeConfigSalt(cfg *config.Config) ([]byte, error) {
lintersSettingsBytes, err := yaml.Marshal(cfg.LintersSettings)
if err != nil {
return nil, fmt.Errorf("failed to json marshal config linter settings: %w", err)
}
configData := bytes.NewBufferString("linters-settings=")
configData.Write(lintersSettingsBytes)
configData.WriteString("\nbuild-tags=%s" + strings.Join(cfg.Run.BuildTags, ","))
h := sha256.New()
if _, err := h.Write(configData.Bytes()); err != nil {
return nil, err
}
return h.Sum(nil), nil
}

View file

@ -8,10 +8,8 @@ import (
"runtime/debug"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/golangci/golangci-lint/pkg/config"
)
type BuildInfo struct {
@ -26,65 +24,75 @@ type versionInfo struct {
BuildInfo *debug.BuildInfo
}
func (e *Executor) initVersion() {
type versionOptions struct {
Format string `mapstructure:"format"`
Debug bool `mapstructure:"debug"`
}
type versionCommand struct {
cmd *cobra.Command
opts versionOptions
info BuildInfo
}
func newVersionCommand(info BuildInfo) *versionCommand {
c := &versionCommand{info: info}
versionCmd := &cobra.Command{
Use: "version",
Short: "Version",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: e.executeVersion,
RunE: c.execute,
}
fs := versionCmd.Flags()
fs.SortFlags = false // sort them as they are defined here
initVersionFlagSet(fs, e.cfg)
fs.StringVar(&c.opts.Format, "format", "", color.GreenString("The version's format can be: 'short', 'json'"))
fs.BoolVar(&c.opts.Debug, "debug", false, color.GreenString("Add build information"))
e.rootCmd.AddCommand(versionCmd)
c.cmd = versionCmd
return c
}
func (e *Executor) executeVersion(_ *cobra.Command, _ []string) error {
if e.cfg.Version.Debug {
func (c *versionCommand) execute(_ *cobra.Command, _ []string) error {
if c.opts.Debug {
info, ok := debug.ReadBuildInfo()
if !ok {
return nil
}
switch strings.ToLower(e.cfg.Version.Format) {
switch strings.ToLower(c.opts.Format) {
case "json":
return json.NewEncoder(os.Stdout).Encode(versionInfo{
Info: e.buildInfo,
Info: c.info,
BuildInfo: info,
})
default:
fmt.Println(info.String())
return printVersion(os.Stdout, e.buildInfo)
return printVersion(os.Stdout, c.info)
}
}
switch strings.ToLower(e.cfg.Version.Format) {
switch strings.ToLower(c.opts.Format) {
case "short":
fmt.Println(e.buildInfo.Version)
fmt.Println(c.info.Version)
return nil
case "json":
return json.NewEncoder(os.Stdout).Encode(e.buildInfo)
return json.NewEncoder(os.Stdout).Encode(c.info)
default:
return printVersion(os.Stdout, e.buildInfo)
return printVersion(os.Stdout, c.info)
}
}
func initVersionFlagSet(fs *pflag.FlagSet, cfg *config.Config) {
// Version config
vc := &cfg.Version
fs.StringVar(&vc.Format, "format", "", wh("The version's format can be: 'short', 'json'"))
fs.BoolVar(&vc.Debug, "debug", false, wh("Add build information"))
}
func printVersion(w io.Writer, buildInfo BuildInfo) error {
func printVersion(w io.Writer, info BuildInfo) error {
_, err := fmt.Fprintf(w, "golangci-lint has version %s built with %s from %s on %s\n",
buildInfo.Version, buildInfo.GoVersion, buildInfo.Commit, buildInfo.Date)
info.Version, info.GoVersion, info.Commit, info.Date)
return err
}