package config import ( "errors" "fmt" "os" "path/filepath" "github.com/golangci/golangci-lint/pkg/fsutils" "github.com/golangci/golangci-lint/pkg/logutils" "github.com/spf13/pflag" "github.com/spf13/viper" ) type FlagSetInit func(fs *pflag.FlagSet, cfg *Config) type FileReader struct { log logutils.Log cfg *Config flagSetInit FlagSetInit } func NewFileReader(toCfg *Config, log logutils.Log, flagSetInit FlagSetInit) *FileReader { return &FileReader{ log: log, cfg: toCfg, flagSetInit: flagSetInit, } } func (r *FileReader) Read() error { // 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 := r.parseConfigOption() if err != nil { if err == errConfigDisabled || err == pflag.ErrHelp { return nil } return fmt.Errorf("can't parse --config option: %s", err) } if configFile != "" { viper.SetConfigFile(configFile) } else { r.setupConfigFileSearch(restArgs) } return r.parseConfig() } func (r *FileReader) parseConfig() error { if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { return nil } return fmt.Errorf("can't read viper config: %s", err) } usedConfigFile := viper.ConfigFileUsed() if usedConfigFile == "" { return nil } usedConfigFile, err := fsutils.ShortestRelPath(usedConfigFile, "") if err != nil { r.log.Warnf("Can't pretty print config file path: %s", err) } r.log.Infof("Used config file %s", usedConfigFile) if err := viper.Unmarshal(r.cfg); err != nil { return fmt.Errorf("can't unmarshal config by viper: %s", err) } if err := r.validateConfig(); err != nil { return fmt.Errorf("can't validate config: %s", err) } if r.cfg.InternalTest { // just for testing purposes: to detect config file usage fmt.Fprintln(logutils.StdOut, "test") os.Exit(0) } return nil } func (r *FileReader) validateConfig() error { c := r.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 (r *FileReader) 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 { r.log.Warnf("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 } r.log.Infof("Config search paths: %s", configSearchPaths) viper.SetConfigName(".golangci") for _, p := range configSearchPaths { viper.AddConfigPath(p) } } var errConfigDisabled = errors.New("config is disabled by --no-config") func (r *FileReader) 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) var cfg Config r.flagSetInit(fs, &cfg) 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 } return "", nil, fmt.Errorf("can't parse args: %s", err) } // for `-v` to work until running of preRun function logutils.SetupVerboseLog(r.log, cfg.Run.IsVerbose) configFile := cfg.Run.Config if cfg.Run.NoConfig && configFile != "" { return "", nil, fmt.Errorf("can't combine option --config and --no-config") } if cfg.Run.NoConfig { return "", nil, errConfigDisabled } return configFile, fs.Args(), nil }