From afc4b4344f9edc8b15c449dd40dcf224f2482c00 Mon Sep 17 00:00:00 2001 From: Denis Isaev Date: Tue, 5 Jun 2018 23:54:05 +0300 Subject: [PATCH] #66: properly merge (not overwrite) slice flags from config and command-line --- pkg/commands/root.go | 23 +-- pkg/commands/run.go | 200 ++--------------------- pkg/commands/run_config.go | 216 ++++++++++++++++++++++++ pkg/fsutils/fsutils.go | 2 +- pkg/lint/lintersdb/lintersdb.go | 9 +- test/run_test.go | 280 +++++++++++++++++++++++++++++++- 6 files changed, 531 insertions(+), 199 deletions(-) create mode 100644 pkg/commands/run_config.go diff --git a/pkg/commands/root.go b/pkg/commands/root.go index 888385af..92e491ad 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -7,15 +7,16 @@ import ( "runtime" "runtime/pprof" + "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/printers" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" ) -func (e *Executor) setupLog() { +func setupLog(isVerbose bool) { log.SetFlags(0) // don't print time - if e.cfg.Run.IsVerbose { + if isVerbose { logrus.SetLevel(logrus.InfoLevel) } } @@ -28,7 +29,7 @@ func (e *Executor) persistentPreRun(cmd *cobra.Command, args []string) { runtime.GOMAXPROCS(e.cfg.Run.Concurrency) - e.setupLog() + setupLog(e.cfg.Run.IsVerbose) if e.cfg.Run.CPUProfilePath != "" { f, err := os.Create(e.cfg.Run.CPUProfilePath) @@ -81,16 +82,16 @@ func (e *Executor) initRoot() { PersistentPostRun: e.persistentPostRun, } - e.initRootFlagSet(rootCmd.PersistentFlags()) + initRootFlagSet(rootCmd.PersistentFlags(), e.cfg, e.needVersionOption()) e.rootCmd = rootCmd } -func (e *Executor) initRootFlagSet(fs *pflag.FlagSet) { - fs.BoolVarP(&e.cfg.Run.IsVerbose, "verbose", "v", false, wh("verbose output")) - fs.StringVar(&e.cfg.Run.CPUProfilePath, "cpu-profile-path", "", wh("Path to CPU profile output file")) - fs.StringVar(&e.cfg.Run.MemProfilePath, "mem-profile-path", "", wh("Path to memory profile output file")) - fs.IntVarP(&e.cfg.Run.Concurrency, "concurrency", "j", getDefaultConcurrency(), wh("Concurrency (default NumCPU)")) - if e.date != "" { - fs.BoolVar(&e.cfg.Run.PrintVersion, "version", false, wh("Print version")) +func initRootFlagSet(fs *pflag.FlagSet, cfg *config.Config, needVersionOption bool) { + fs.BoolVarP(&cfg.Run.IsVerbose, "verbose", "v", false, wh("verbose output")) + 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.IntVarP(&cfg.Run.Concurrency, "concurrency", "j", getDefaultConcurrency(), wh("Concurrency (default NumCPU)")) + if needVersionOption { + fs.BoolVar(&cfg.Run.PrintVersion, "version", false, wh("Print version")) } } diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 65762265..97b39374 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -2,20 +2,17 @@ package commands import ( "context" - "errors" "fmt" "go/token" "io/ioutil" "log" "os" - "path/filepath" "runtime" "strings" "time" "github.com/fatih/color" "github.com/golangci/golangci-lint/pkg/config" - "github.com/golangci/golangci-lint/pkg/fsutils" "github.com/golangci/golangci-lint/pkg/lint" "github.com/golangci/golangci-lint/pkg/lint/lintersdb" "github.com/golangci/golangci-lint/pkg/printers" @@ -24,7 +21,6 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/spf13/viper" ) const ( @@ -48,7 +44,7 @@ func wh(text string) string { return color.GreenString(text) } -func (e *Executor) initFlagSet(fs *pflag.FlagSet) { +func initFlagSet(fs *pflag.FlagSet, cfg *config.Config) { hideFlag := func(name string) { if err := fs.MarkHidden(name); err != nil { panic(err) @@ -56,7 +52,7 @@ func (e *Executor) initFlagSet(fs *pflag.FlagSet) { } // Output config - oc := &e.cfg.Output + oc := &cfg.Output fs.StringVar(&oc.Format, "out-format", config.OutFormatColoredLineNumber, wh(fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|")))) @@ -66,10 +62,10 @@ func (e *Executor) initFlagSet(fs *pflag.FlagSet) { hideFlag("print-welcome") // no longer used // Run config - rc := &e.cfg.Run + rc := &cfg.Run fs.IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code", 1, wh("Exit code when issues were found")) - fs.StringSliceVar(&rc.BuildTags, "build-tags", []string{}, wh("Build tags (not all linters support them)")) + fs.StringSliceVar(&rc.BuildTags, "build-tags", nil, wh("Build tags (not all linters support them)")) fs.DurationVar(&rc.Deadline, "deadline", time.Minute, wh("Deadline 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")) @@ -77,7 +73,7 @@ func (e *Executor) initFlagSet(fs *pflag.FlagSet) { fs.BoolVar(&rc.NoConfig, "no-config", false, wh("Don't read config")) // Linters settings config - lsc := &e.cfg.LintersSettings + lsc := &cfg.LintersSettings // Hide all linters settings flags: they were initially visible, // but when number of linters started to grow it became ovious that @@ -126,18 +122,18 @@ func (e *Executor) initFlagSet(fs *pflag.FlagSet) { hideFlag("depguard.include-go-root") // Linters config - lc := &e.cfg.Linters - fs.StringSliceVarP(&lc.Enable, "enable", "E", []string{}, wh("Enable specific linter")) - fs.StringSliceVarP(&lc.Disable, "disable", "D", []string{}, wh("Disable specific linter")) + lc := &cfg.Linters + fs.StringSliceVarP(&lc.Enable, "enable", "E", nil, wh("Enable specific linter")) + fs.StringSliceVarP(&lc.Disable, "disable", "D", nil, wh("Disable specific linter")) fs.BoolVar(&lc.EnableAll, "enable-all", false, wh("Enable all linters")) fs.BoolVar(&lc.DisableAll, "disable-all", false, wh("Disable all linters")) - fs.StringSliceVarP(&lc.Presets, "presets", "p", []string{}, + fs.StringSliceVarP(&lc.Presets, "presets", "p", nil, wh(fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint linters' to see them. This option implies option --disable-all", strings.Join(lintersdb.AllPresets(), "|")))) fs.BoolVar(&lc.Fast, "fast", false, wh("Run only fast linters from enabled linters set")) // Issues config - ic := &e.cfg.Issues - fs.StringSliceVarP(&ic.ExcludePatterns, "exclude", "e", []string{}, wh("Exclude issue by regexp")) + ic := &cfg.Issues + fs.StringSliceVarP(&ic.ExcludePatterns, "exclude", "e", nil, wh("Exclude issue by regexp")) fs.BoolVar(&ic.UseDefaultExcludes, "exclude-use-default", true, getDefaultExcludeHelp()) fs.IntVar(&ic.MaxIssuesPerLinter, "max-issues-per-linter", 50, wh("Maximum issues count per one linter. Set to 0 to disable")) @@ -162,9 +158,15 @@ func (e *Executor) initRun() { fs := runCmd.Flags() fs.SortFlags = false // sort them as they are defined here - e.initFlagSet(fs) + initFlagSet(fs, e.cfg) + // 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. e.parseConfig() + + // Slice options must be explicitly set for properly merging. + fixSlicesFlags(fs) } func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan result.Issue, error) { @@ -289,172 +291,6 @@ func (e *Executor) executeRun(cmd *cobra.Command, args []string) { } } -func (e *Executor) parseConfig() { - // XXX: hack with double parsing for 2 purposes: - // 1. to access "config" option here. - // 2. to give config less priority than command line. - - // 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) - - // Don't do `fs.AddFlagSet(cmd.Flags())` because it shared flags representations: - // `changed` variable inside string slice vars will be shared. - e.initFlagSet(fs) - e.initRootFlagSet(fs) - - fs.Usage = func() {} // otherwise help text will be printed twice - if err := fs.Parse(os.Args); err != nil { - if err == pflag.ErrHelp { - return - } - logrus.Fatalf("Can't parse args: %s", err) - } - - e.setupLog() // for `-v` to work until running of preRun function - - if err := viper.BindPFlags(fs); err != nil { - logrus.Fatalf("Can't bind cobra's flags to viper: %s", err) - } - - viper.SetEnvPrefix("GOLANGCI") - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - viper.AutomaticEnv() - - configFile := e.cfg.Run.Config - if e.cfg.Run.NoConfig && configFile != "" { - logrus.Fatal("can't combine option --config and --no-config") - } - - if e.cfg.Run.NoConfig { - return - } - - if configFile != "" { - viper.SetConfigFile(configFile) - } else { - setupConfigFileSearch(fs.Args()) - } - - e.parseConfigImpl() -} - -func setupConfigFileSearch(args []string) { - // skip all args ([golangci-lint, run/linters]) before files/dirs list - for len(args) != 0 { - if args[0] == "run" { - args = args[1:] - break - } - - args = args[1:] - } - - // find first file/dir arg - firstArg := "./..." - if len(args) != 0 { - firstArg = args[0] - } - - absStartPath, err := filepath.Abs(firstArg) - if err != nil { - logrus.Infof("Can't make abs path for %q: %s", firstArg, err) - absStartPath = filepath.Clean(firstArg) - } - - // start from it - var curDir string - if fsutils.IsDir(absStartPath) { - curDir = absStartPath - } else { - curDir = filepath.Dir(absStartPath) - } - - // find all dirs from it up to the root - configSearchPaths := []string{"./"} - for { - configSearchPaths = append(configSearchPaths, curDir) - newCurDir := filepath.Dir(curDir) - if curDir == newCurDir || newCurDir == "" { - break - } - curDir = newCurDir - } - - logrus.Infof("Config search paths: %s", configSearchPaths) - viper.SetConfigName(".golangci") - for _, p := range configSearchPaths { - viper.AddConfigPath(p) - } -} - -func getRelPath(p string) string { - wd, err := os.Getwd() - if err != nil { - logrus.Infof("Can't get wd: %s", err) - return p - } - - r, err := filepath.Rel(wd, p) - if err != nil { - logrus.Infof("Can't make path %s relative to %s: %s", p, wd, err) - return p - } - - return r -} - -func (e *Executor) parseConfigImpl() { - commandLineConfig := *e.cfg // make copy - - if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - return - } - logrus.Fatalf("Can't read viper config: %s", err) - } - - usedConfigFile := viper.ConfigFileUsed() - if usedConfigFile == "" { - return - } - logrus.Infof("Used config file %s", getRelPath(usedConfigFile)) - - if err := viper.Unmarshal(&e.cfg); err != nil { - logrus.Fatalf("Can't unmarshal config by viper: %s", err) - } - - if err := e.validateConfig(&commandLineConfig); err != nil { - logrus.Fatal(err) - } - - if e.cfg.InternalTest { // just for testing purposes: to detect config file usage - fmt.Fprintln(printers.StdOut, "test") - os.Exit(0) - } -} - -func (e *Executor) validateConfig(commandLineConfig *config.Config) error { - c := e.cfg - if len(c.Run.Args) != 0 { - return errors.New("option run.args in config isn't supported now") - } - - if commandLineConfig.Run.CPUProfilePath == "" && c.Run.CPUProfilePath != "" { - return errors.New("option run.cpuprofilepath in config isn't allowed") - } - - if commandLineConfig.Run.MemProfilePath == "" && c.Run.MemProfilePath != "" { - return errors.New("option run.memprofilepath in config isn't allowed") - } - - if !commandLineConfig.Run.IsVerbose && c.Run.IsVerbose { - return errors.New("can't set run.verbose option with config: only on command-line") - } - - return nil -} - func watchResources(ctx context.Context, done chan struct{}) { startedAt := time.Now() diff --git a/pkg/commands/run_config.go b/pkg/commands/run_config.go new file mode 100644 index 00000000..4aa2bb4f --- /dev/null +++ b/pkg/commands/run_config.go @@ -0,0 +1,216 @@ +package commands + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/golangci/golangci-lint/pkg/config" + "github.com/golangci/golangci-lint/pkg/fsutils" + "github.com/golangci/golangci-lint/pkg/printers" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +func (e *Executor) parseConfigImpl() { + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + return + } + logrus.Fatalf("Can't read viper config: %s", err) + } + + usedConfigFile := viper.ConfigFileUsed() + if usedConfigFile == "" { + return + } + logrus.Infof("Used config file %s", getRelPath(usedConfigFile)) + + if err := viper.Unmarshal(&e.cfg); err != nil { + logrus.Fatalf("Can't unmarshal config by viper: %s", err) + } + + if err := e.validateConfig(); err != nil { + logrus.Fatal(err) + } + + if e.cfg.InternalTest { // just for testing purposes: to detect config file usage + fmt.Fprintln(printers.StdOut, "test") + os.Exit(0) + } +} + +func (e *Executor) validateConfig() error { + c := e.cfg + if len(c.Run.Args) != 0 { + return errors.New("option run.args in config isn't supported now") + } + + if c.Run.CPUProfilePath != "" { + return errors.New("option run.cpuprofilepath in config isn't allowed") + } + + if c.Run.MemProfilePath != "" { + return errors.New("option run.memprofilepath in config isn't allowed") + } + + if c.Run.IsVerbose { + return errors.New("can't set run.verbose option with config: only on command-line") + } + + return nil +} + +func setupConfigFileSearch(args []string) { + // skip all args ([golangci-lint, run/linters]) before files/dirs list + for len(args) != 0 { + if args[0] == "run" { + args = args[1:] + break + } + + args = args[1:] + } + + // find first file/dir arg + firstArg := "./..." + if len(args) != 0 { + firstArg = args[0] + } + + absStartPath, err := filepath.Abs(firstArg) + if err != nil { + logrus.Infof("Can't make abs path for %q: %s", firstArg, err) + absStartPath = filepath.Clean(firstArg) + } + + // start from it + var curDir string + if fsutils.IsDir(absStartPath) { + curDir = absStartPath + } else { + curDir = filepath.Dir(absStartPath) + } + + // find all dirs from it up to the root + configSearchPaths := []string{"./"} + for { + configSearchPaths = append(configSearchPaths, curDir) + newCurDir := filepath.Dir(curDir) + if curDir == newCurDir || newCurDir == "" { + break + } + curDir = newCurDir + } + + logrus.Infof("Config search paths: %s", configSearchPaths) + viper.SetConfigName(".golangci") + for _, p := range configSearchPaths { + viper.AddConfigPath(p) + } +} + +func getRelPath(p string) string { + wd, err := os.Getwd() + if err != nil { + logrus.Infof("Can't get wd: %s", err) + return p + } + + r, err := filepath.Rel(wd, p) + if err != nil { + logrus.Infof("Can't make path %s relative to %s: %s", p, wd, err) + return p + } + + return r +} + +func (e *Executor) needVersionOption() bool { + return e.date != "" +} + +func parseConfigOption() (string, []string, 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) + + // 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. + var cfg config.Config + initFlagSet(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 err == pflag.ErrHelp { + return "", nil, err + } + + logrus.Fatalf("Can't parse args: %s", err) + } + + setupLog(cfg.Run.IsVerbose) // for `-v` to work until running of preRun function + + configFile := cfg.Run.Config + if cfg.Run.NoConfig && configFile != "" { + logrus.Fatal("can't combine option --config and --no-config") + } + + if cfg.Run.NoConfig { + return "", nil, fmt.Errorf("no need to use config") + } + + return configFile, fs.Args(), nil +} + +func (e *Executor) parseConfig() { + // XXX: hack with double parsing for 2 purposes: + // 1. to access "config" option here. + // 2. to give config less priority than command line. + + configFile, restArgs, err := parseConfigOption() + if err != nil { + return // skippable error, e.g. --no-config + } + + if configFile != "" { + viper.SetConfigFile(configFile) + } else { + setupConfigFileSearch(restArgs) + } + + e.parseConfigImpl() +} + +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 + } + + // calling Set sets Changed to true: next Set calls will append, not overwrite + _ = f.Value.Set(strings.Join(s, ",")) + }) +} diff --git a/pkg/fsutils/fsutils.go b/pkg/fsutils/fsutils.go index 04b14070..3de4d7ef 100644 --- a/pkg/fsutils/fsutils.go +++ b/pkg/fsutils/fsutils.go @@ -117,7 +117,7 @@ func GetPathsForAnalysis(ctx context.Context, inputPaths []string, includeTests pr := NewPathResolver(stdExcludeDirs, []string{".go"}, includeTests) paths, err := pr.Resolve(inputPaths...) if err != nil { - return nil, fmt.Errorf("can't resolve paths: %s", err) + return nil, fmt.Errorf("can't resolve paths %v: %s", inputPaths, err) } return processResolvedPaths(paths) diff --git a/pkg/lint/lintersdb/lintersdb.go b/pkg/lint/lintersdb/lintersdb.go index d8c43890..a1856fca 100644 --- a/pkg/lint/lintersdb/lintersdb.go +++ b/pkg/lint/lintersdb/lintersdb.go @@ -3,6 +3,7 @@ package lintersdb import ( "fmt" "os" + "sort" "strings" "sync" @@ -192,7 +193,7 @@ func GetAllSupportedLinterConfigs() []linter.Config { }) } -func getAllEnabledByDefaultLinters() []linter.Config { +func GetAllEnabledByDefaultLinters() []linter.Config { var ret []linter.Config for _, lc := range GetAllSupportedLinterConfigs() { if lc.EnabledByDefault { @@ -402,7 +403,7 @@ func GetEnabledLinters(cfg *config.Config) ([]linter.Config, error) { return nil, err } - resultLintersSet := getEnabledLintersSet(&cfg.Linters, getAllEnabledByDefaultLinters()) + resultLintersSet := getEnabledLintersSet(&cfg.Linters, GetAllEnabledByDefaultLinters()) var resultLinters []linter.Config for _, lc := range resultLintersSet { @@ -419,9 +420,11 @@ func verbosePrintLintersStatus(cfg *config.Config, lcs []linter.Config) { for _, lc := range lcs { linterNames = append(linterNames, lc.Linter.Name()) } - logrus.Infof("Active linters: %s", linterNames) + sort.StringSlice(linterNames).Sort() + logrus.Infof("Active %d linters: %s", len(linterNames), linterNames) if len(cfg.Linters.Presets) != 0 { + sort.StringSlice(cfg.Linters.Presets).Sort() logrus.Infof("Active presets: %s", cfg.Linters.Presets) } } diff --git a/test/run_test.go b/test/run_test.go index e277940e..b1af7ed8 100644 --- a/test/run_test.go +++ b/test/run_test.go @@ -1,12 +1,20 @@ package test import ( + "fmt" + "io/ioutil" + "log" + "os" "os/exec" "path/filepath" + "sort" + "strings" "sync" "syscall" "testing" + "github.com/golangci/golangci-lint/pkg/lint/lintersdb" + "github.com/stretchr/testify/assert" ) @@ -32,15 +40,17 @@ func TestCongratsMessageIfNoIssues(t *testing.T) { func TestDeadline(t *testing.T) { out, exitCode := runGolangciLint(t, "--deadline=1ms", "../...") assert.Equal(t, 4, exitCode) - assert.Equal(t, "", out) // no 'Congrats! No issues were found.' + assert.Contains(t, out, "deadline exceeded: try increase it by passing --deadline option") + assert.NotContains(t, out, "Congrats! No issues were found.") } func runGolangciLint(t *testing.T, args ...string) (string, int) { installBinary(t) runArgs := append([]string{"run"}, args...) + log.Printf("golangci-lint %s", strings.Join(runArgs, " ")) cmd := exec.Command("golangci-lint", runArgs...) - out, err := cmd.Output() + out, err := cmd.CombinedOutput() if err != nil { if exitError, ok := err.(*exec.ExitError); ok { t.Logf("stderr: %s", exitError.Stderr) @@ -57,6 +67,34 @@ func runGolangciLint(t *testing.T, args ...string) (string, int) { return string(out), ws.ExitStatus() } +func runGolangciLintWithYamlConfig(t *testing.T, cfg string, args ...string) string { + out, ec := runGolangciLintWithYamlConfigWithCode(t, cfg, args...) + assert.Equal(t, 0, ec) + + return out +} + +func runGolangciLintWithYamlConfigWithCode(t *testing.T, cfg string, args ...string) (string, int) { + f, err := ioutil.TempFile("", "golangci_lint_test") + assert.NoError(t, err) + f.Close() + + cfgPath := f.Name() + ".yml" + err = os.Rename(f.Name(), cfgPath) + assert.NoError(t, err) + + defer os.Remove(cfgPath) + + cfg = strings.TrimSpace(cfg) + cfg = strings.Replace(cfg, "\t", " ", -1) + + err = ioutil.WriteFile(cfgPath, []byte(cfg), os.ModePerm) + assert.NoError(t, err) + + pargs := append([]string{"-c", cfgPath}, args...) + return runGolangciLint(t, pargs...) +} + func TestTestsAreLintedByDefault(t *testing.T) { out, exitCode := runGolangciLint(t, "./testdata/withtests") assert.Equal(t, 0, exitCode, out) @@ -74,3 +112,241 @@ func TestConfigFileIsDetected(t *testing.T) { out, exitCode := runGolangciLint(t) // doesn't detect when no args checkNoIssuesRun(t, out, exitCode) } + +func inSlice(s []string, v string) bool { + for _, sv := range s { + if sv == v { + return true + } + } + + return false +} + +func getEnabledByDefaultFastLintersExcept(except ...string) []string { + ebdl := lintersdb.GetAllEnabledByDefaultLinters() + ret := []string{} + for _, linter := range ebdl { + if linter.DoesFullImport { + continue + } + + if !inSlice(except, linter.Linter.Name()) { + ret = append(ret, linter.Linter.Name()) + } + } + + return ret +} + +func getEnabledByDefaultLinters() []string { + ebdl := lintersdb.GetAllEnabledByDefaultLinters() + ret := []string{} + for _, linter := range ebdl { + ret = append(ret, linter.Linter.Name()) + } + + return ret +} + +func getEnabledByDefaultFastLintersWith(with ...string) []string { + ebdl := lintersdb.GetAllEnabledByDefaultLinters() + ret := append([]string{}, with...) + for _, linter := range ebdl { + if linter.DoesFullImport { + continue + } + + ret = append(ret, linter.Linter.Name()) + } + + return ret +} + +func mergeMegacheck(linters []string) []string { + if inSlice(linters, "staticcheck") && + inSlice(linters, "gosimple") && + inSlice(linters, "unused") { + ret := []string{"megacheck"} + for _, linter := range linters { + if !inSlice([]string{"staticcheck", "gosimple", "unused"}, linter) { + ret = append(ret, linter) + } + } + + return ret + } + + return linters +} + +func TestEnabledLinters(t *testing.T) { + type tc struct { + name string + cfg string + el []string + args string + noImplicitFast bool + } + + cases := []tc{ + { + name: "disable govet in config", + cfg: ` + linters: + disable: + - govet + `, + el: getEnabledByDefaultFastLintersExcept("govet"), + }, + { + name: "enable golint in config", + cfg: ` + linters: + enable: + - golint + `, + el: getEnabledByDefaultFastLintersWith("golint"), + }, + { + name: "disable govet in cmd", + args: "-Dgovet", + el: getEnabledByDefaultFastLintersExcept("govet"), + }, + { + name: "enable gofmt in cmd and enable golint in config", + args: "-Egofmt", + cfg: ` + linters: + enable: + - golint + `, + el: getEnabledByDefaultFastLintersWith("golint", "gofmt"), + }, + { + name: "fast option in config", + cfg: ` + linters: + fast: true + `, + el: getEnabledByDefaultFastLintersWith(), + noImplicitFast: true, + }, + { + name: "explicitly unset fast option in config", + cfg: ` + linters: + fast: false + `, + el: getEnabledByDefaultLinters(), + noImplicitFast: true, + }, + { + name: "set fast option in command-line", + args: "--fast", + el: getEnabledByDefaultFastLintersWith(), + noImplicitFast: true, + }, + { + name: "fast option in command-line has higher priority to enable", + cfg: ` + linters: + fast: false + `, + args: "--fast", + el: getEnabledByDefaultFastLintersWith(), + noImplicitFast: true, + }, + { + name: "fast option in command-line has higher priority to disable", + cfg: ` + linters: + fast: true + `, + args: "--fast=false", + el: getEnabledByDefaultLinters(), + noImplicitFast: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + runArgs := []string{"-v"} + if !c.noImplicitFast { + runArgs = append(runArgs, "--fast") + } + if c.args != "" { + runArgs = append(runArgs, strings.Split(c.args, " ")...) + } + out := runGolangciLintWithYamlConfig(t, c.cfg, runArgs...) + el := mergeMegacheck(c.el) + sort.StringSlice(el).Sort() + expectedLine := fmt.Sprintf("Active %d linters: [%s]", len(el), strings.Join(el, " ")) + assert.Contains(t, out, expectedLine) + }) + } +} + +func TestEnabledPresetsAreNotDuplicated(t *testing.T) { + out, ec := runGolangciLint(t, "--no-config", "-v", "-p", "style,bugs") + assert.Equal(t, 0, ec) + assert.Contains(t, out, "Active presets: [bugs style]") +} + +func TestDisallowedOptionsInConfig(t *testing.T) { + type tc struct { + cfg string + option string + } + + cases := []tc{ + { + cfg: ` + ruN: + Args: + - 1 + `, + }, + { + cfg: ` + run: + CPUProfilePath: path + `, + option: "--cpu-profile-path=path", + }, + { + cfg: ` + run: + MemProfilePath: path + `, + option: "--mem-profile-path=path", + }, + { + cfg: ` + run: + Verbose: true + `, + option: "-v", + }, + } + + for _, c := range cases { + // Run with disallowed option set only in config + _, ec := runGolangciLintWithYamlConfigWithCode(t, c.cfg) + assert.Equal(t, 1, ec) + + if c.option == "" { + continue + } + + args := []string{c.option, "--fast"} + + // Run with disallowed option set only in command-line + _, ec = runGolangciLint(t, args...) + assert.Equal(t, 0, ec) + + // Run with disallowed option set both in command-line and in config + _, ec = runGolangciLintWithYamlConfigWithCode(t, c.cfg, args...) + assert.Equal(t, 1, ec) + } +}