diff --git a/README.md b/README.md index 9903c92d..1ef34947 100644 --- a/README.md +++ b/README.md @@ -288,10 +288,16 @@ GolangCI-Lint looks for next config paths in the current directory: - `.golangci.toml` - `.golangci.json` +GolangCI-Lint also searches config file in all directories from directory of the first analyzed path up to the root. +To see which config file is used and where it was searched run golangci-lint with `-v` option. + Configuration options inside the file are identical to command-line options. +You can configure specific linters options only within configuration file, it can't be done with command-line. + There is a [`.golangci.yml`](https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml) with all supported options. -It's a [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) of this repo: we enable more linters than by default and make their settings more strict: +It's a [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) of this repo: we enable more linters +than by default and make their settings more strict: ```yaml linters-settings: govet: diff --git a/README.md.tmpl b/README.md.tmpl index 6f23f1fe..95b10bf0 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -180,10 +180,16 @@ GolangCI-Lint looks for next config paths in the current directory: - `.golangci.toml` - `.golangci.json` +GolangCI-Lint also searches config file in all directories from directory of the first analyzed path up to the root. +To see which config file is used and where it was searched run golangci-lint with `-v` option. + Configuration options inside the file are identical to command-line options. +You can configure specific linters options only within configuration file, it can't be done with command-line. + There is a [`.golangci.yml`](https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml) with all supported options. -It's a [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) of this repo: we enable more linters than by default and make their settings more strict: +It's a [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) of this repo: we enable more linters +than by default and make their settings more strict: ```yaml {{.GolangciYaml}} ``` diff --git a/pkg/commands/executor.go b/pkg/commands/executor.go index ee8fdfb9..4b42395f 100644 --- a/pkg/commands/executor.go +++ b/pkg/commands/executor.go @@ -2,6 +2,7 @@ package commands import ( "github.com/golangci/golangci-lint/pkg/config" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -20,6 +21,8 @@ func NewExecutor(version, commit, date string) *Executor { cfg: &config.Config{}, } + logrus.SetLevel(logrus.WarnLevel) + e.initRoot() e.initRun() e.initLinters() diff --git a/pkg/commands/root.go b/pkg/commands/root.go index b8c87b29..19b8cedf 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -13,7 +13,14 @@ import ( "github.com/spf13/pflag" ) -func (e *Executor) persistentPostRun(cmd *cobra.Command, args []string) { +func (e *Executor) setupLog() { + log.SetFlags(0) // don't print time + if e.cfg.Run.IsVerbose { + logrus.SetLevel(logrus.InfoLevel) + } +} + +func (e *Executor) persistentPreRun(cmd *cobra.Command, args []string) { if e.cfg.Run.PrintVersion { fmt.Fprintf(printers.StdOut, "golangci-lint has version %s built from %s on %s\n", e.version, e.commit, e.date) os.Exit(0) @@ -21,12 +28,7 @@ func (e *Executor) persistentPostRun(cmd *cobra.Command, args []string) { runtime.GOMAXPROCS(e.cfg.Run.Concurrency) - log.SetFlags(0) // don't print time - if e.cfg.Run.IsVerbose { - logrus.SetLevel(logrus.InfoLevel) - } else { - logrus.SetLevel(logrus.WarnLevel) - } + e.setupLog() if e.cfg.Run.CPUProfilePath != "" { f, err := os.Create(e.cfg.Run.CPUProfilePath) @@ -39,7 +41,7 @@ func (e *Executor) persistentPostRun(cmd *cobra.Command, args []string) { } } -func (e *Executor) persistentPreRun(cmd *cobra.Command, args []string) { +func (e *Executor) persistentPostRun(cmd *cobra.Command, args []string) { if e.cfg.Run.CPUProfilePath != "" { pprof.StopCPUProfile() } @@ -75,8 +77,8 @@ func (e *Executor) initRoot() { logrus.Fatal(err) } }, - PersistentPreRun: e.persistentPostRun, - PersistentPostRun: e.persistentPreRun, + PersistentPreRun: e.persistentPreRun, + PersistentPostRun: e.persistentPostRun, } e.initRootFlagSet(rootCmd.PersistentFlags()) diff --git a/pkg/commands/run.go b/pkg/commands/run.go index dcc7ac66..a5ec9cb7 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -8,12 +8,14 @@ import ( "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" @@ -301,6 +303,8 @@ func (e *Executor) parseConfig() { 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) } @@ -318,16 +322,80 @@ func (e *Executor) parseConfig() { return } - if configFile == "" { - viper.SetConfigName(".golangci") - viper.AddConfigPath("./") - } else { + 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 @@ -338,6 +406,12 @@ func (e *Executor) parseConfigImpl() { 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) } @@ -345,6 +419,11 @@ func (e *Executor) parseConfigImpl() { 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 { diff --git a/pkg/config/config.go b/pkg/config/config.go index f3405e2c..08654ecd 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -164,7 +164,7 @@ type Issues struct { Diff bool `mapstructure:"new"` } -type Config struct { // nolint:maligned +type Config struct { //nolint:maligned Run Run Output struct { @@ -177,4 +177,6 @@ type Config struct { // nolint:maligned LintersSettings LintersSettings `mapstructure:"linters-settings"` Linters Linters Issues Issues + + InternalTest bool // Option is used only for testing golangci-lint code, don't use it } diff --git a/test/linters_test.go b/test/linters_test.go index 9a9ce1ce..3488ac24 100644 --- a/test/linters_test.go +++ b/test/linters_test.go @@ -41,6 +41,7 @@ func TestSourcesFromTestdataWithIssuesDir(t *testing.T) { func testOneSource(t *testing.T, sourcePath string) { goErrchkBin := filepath.Join(runtime.GOROOT(), "test", "errchk") cmd := exec.Command(goErrchkBin, binName, "run", + "--no-config", "--enable-all", "--dupl.threshold=20", "--gocyclo.min-complexity=20", diff --git a/test/run_test.go b/test/run_test.go index 2f15f312..e277940e 100644 --- a/test/run_test.go +++ b/test/run_test.go @@ -19,14 +19,18 @@ func installBinary(t assert.TestingT) { }) } -func TestCongratsMessageIfNoIssues(t *testing.T) { - out, exitCode := runGolangciLint(t, "../...") +func checkNoIssuesRun(t *testing.T, out string, exitCode int) { assert.Equal(t, 0, exitCode) assert.Equal(t, "Congrats! No issues were found.\n", out) } +func TestCongratsMessageIfNoIssues(t *testing.T) { + out, exitCode := runGolangciLint(t, "../...") + checkNoIssuesRun(t, out, exitCode) +} + func TestDeadline(t *testing.T) { - out, exitCode := runGolangciLint(t, "--no-config", "--deadline=1ms", "../...") + out, exitCode := runGolangciLint(t, "--deadline=1ms", "../...") assert.Equal(t, 4, exitCode) assert.Equal(t, "", out) // no 'Congrats! No issues were found.' } @@ -54,6 +58,19 @@ func runGolangciLint(t *testing.T, args ...string) (string, int) { } func TestTestsAreLintedByDefault(t *testing.T) { - out, exitCode := runGolangciLint(t, "--no-config", "./testdata/withtests") + out, exitCode := runGolangciLint(t, "./testdata/withtests") assert.Equal(t, 0, exitCode, out) } + +func TestConfigFileIsDetected(t *testing.T) { + checkGotConfig := func(out string, exitCode int) { + assert.Equal(t, 0, exitCode, out) + assert.Equal(t, "test\n", out) // test config contains InternalTest: true, it triggers such output + } + + checkGotConfig(runGolangciLint(t, "testdata/withconfig/pkg")) + checkGotConfig(runGolangciLint(t, "testdata/withconfig/...")) + + out, exitCode := runGolangciLint(t) // doesn't detect when no args + checkNoIssuesRun(t, out, exitCode) +} diff --git a/test/testdata/withconfig/.golangci.yml b/test/testdata/withconfig/.golangci.yml new file mode 100644 index 00000000..db94b3b0 --- /dev/null +++ b/test/testdata/withconfig/.golangci.yml @@ -0,0 +1 @@ +InternalTest: true \ No newline at end of file diff --git a/test/testdata/withconfig/pkg/pkg.go b/test/testdata/withconfig/pkg/pkg.go new file mode 100644 index 00000000..70bb1457 --- /dev/null +++ b/test/testdata/withconfig/pkg/pkg.go @@ -0,0 +1,3 @@ +package pkg + +func SomeTestFunc() {}