golangci-lint/pkg/commands/run_config.go
2018-06-08 08:43:43 +03:00

218 lines
5.2 KiB
Go

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/logutils"
"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 {
logutils.HiddenWarnf("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 {
logutils.HiddenWarnf("Can't get wd: %s", err)
return p
}
r, err := filepath.Rel(wd, p)
if err != nil {
logutils.HiddenWarnf("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, ","))
})
}