From 0f6213dbc2483641c9d31651ce1cd5d6d2bfd87e Mon Sep 17 00:00:00 2001
From: Denis Isaev <denis@golangci.com>
Date: Sat, 2 Jun 2018 19:42:29 +0300
Subject: [PATCH] #60: search config file in directories from file path up to
 root

---
 README.md                              |  8 ++-
 README.md.tmpl                         |  8 ++-
 pkg/commands/executor.go               |  3 +
 pkg/commands/root.go                   | 22 ++++---
 pkg/commands/run.go                    | 87 ++++++++++++++++++++++++--
 pkg/config/config.go                   |  4 +-
 test/linters_test.go                   |  1 +
 test/run_test.go                       | 25 ++++++--
 test/testdata/withconfig/.golangci.yml |  1 +
 test/testdata/withconfig/pkg/pkg.go    |  3 +
 10 files changed, 141 insertions(+), 21 deletions(-)
 create mode 100644 test/testdata/withconfig/.golangci.yml
 create mode 100644 test/testdata/withconfig/pkg/pkg.go

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() {}