github.com/shulhan/golangci-lint@v1.10.1/pkg/config/reader.go (about)

     1  package config
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"github.com/golangci/golangci-lint/pkg/fsutils"
    10  	"github.com/golangci/golangci-lint/pkg/logutils"
    11  	"github.com/spf13/pflag"
    12  	"github.com/spf13/viper"
    13  )
    14  
    15  type FlagSetInit func(fs *pflag.FlagSet, cfg *Config)
    16  
    17  type FileReader struct {
    18  	log         logutils.Log
    19  	cfg         *Config
    20  	flagSetInit FlagSetInit
    21  }
    22  
    23  func NewFileReader(toCfg *Config, log logutils.Log, flagSetInit FlagSetInit) *FileReader {
    24  	return &FileReader{
    25  		log:         log,
    26  		cfg:         toCfg,
    27  		flagSetInit: flagSetInit,
    28  	}
    29  }
    30  
    31  func (r *FileReader) Read() error {
    32  	// XXX: hack with double parsing for 2 purposes:
    33  	// 1. to access "config" option here.
    34  	// 2. to give config less priority than command line.
    35  
    36  	configFile, restArgs, err := r.parseConfigOption()
    37  	if err != nil {
    38  		if err == errConfigDisabled || err == pflag.ErrHelp {
    39  			return nil
    40  		}
    41  
    42  		return fmt.Errorf("can't parse --config option: %s", err)
    43  	}
    44  
    45  	if configFile != "" {
    46  		viper.SetConfigFile(configFile)
    47  	} else {
    48  		r.setupConfigFileSearch(restArgs)
    49  	}
    50  
    51  	return r.parseConfig()
    52  }
    53  
    54  func (r *FileReader) parseConfig() error {
    55  	if err := viper.ReadInConfig(); err != nil {
    56  		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
    57  			return nil
    58  		}
    59  
    60  		return fmt.Errorf("can't read viper config: %s", err)
    61  	}
    62  
    63  	usedConfigFile := viper.ConfigFileUsed()
    64  	if usedConfigFile == "" {
    65  		return nil
    66  	}
    67  
    68  	usedConfigFile, err := fsutils.ShortestRelPath(usedConfigFile, "")
    69  	if err != nil {
    70  		r.log.Warnf("Can't pretty print config file path: %s", err)
    71  	}
    72  	r.log.Infof("Used config file %s", usedConfigFile)
    73  
    74  	if err := viper.Unmarshal(r.cfg); err != nil {
    75  		return fmt.Errorf("can't unmarshal config by viper: %s", err)
    76  	}
    77  
    78  	if err := r.validateConfig(); err != nil {
    79  		return fmt.Errorf("can't validate config: %s", err)
    80  	}
    81  
    82  	if r.cfg.InternalTest { // just for testing purposes: to detect config file usage
    83  		fmt.Fprintln(logutils.StdOut, "test")
    84  		os.Exit(0)
    85  	}
    86  
    87  	return nil
    88  }
    89  
    90  func (r *FileReader) validateConfig() error {
    91  	c := r.cfg
    92  	if len(c.Run.Args) != 0 {
    93  		return errors.New("option run.args in config isn't supported now")
    94  	}
    95  
    96  	if c.Run.CPUProfilePath != "" {
    97  		return errors.New("option run.cpuprofilepath in config isn't allowed")
    98  	}
    99  
   100  	if c.Run.MemProfilePath != "" {
   101  		return errors.New("option run.memprofilepath in config isn't allowed")
   102  	}
   103  
   104  	if c.Run.IsVerbose {
   105  		return errors.New("can't set run.verbose option with config: only on command-line")
   106  	}
   107  
   108  	return nil
   109  }
   110  
   111  func (r *FileReader) setupConfigFileSearch(args []string) {
   112  	// skip all args ([golangci-lint, run/linters]) before files/dirs list
   113  	for len(args) != 0 {
   114  		if args[0] == "run" {
   115  			args = args[1:]
   116  			break
   117  		}
   118  
   119  		args = args[1:]
   120  	}
   121  
   122  	// find first file/dir arg
   123  	firstArg := "./..."
   124  	if len(args) != 0 {
   125  		firstArg = args[0]
   126  	}
   127  
   128  	absStartPath, err := filepath.Abs(firstArg)
   129  	if err != nil {
   130  		r.log.Warnf("Can't make abs path for %q: %s", firstArg, err)
   131  		absStartPath = filepath.Clean(firstArg)
   132  	}
   133  
   134  	// start from it
   135  	var curDir string
   136  	if fsutils.IsDir(absStartPath) {
   137  		curDir = absStartPath
   138  	} else {
   139  		curDir = filepath.Dir(absStartPath)
   140  	}
   141  
   142  	// find all dirs from it up to the root
   143  	configSearchPaths := []string{"./"}
   144  	for {
   145  		configSearchPaths = append(configSearchPaths, curDir)
   146  		newCurDir := filepath.Dir(curDir)
   147  		if curDir == newCurDir || newCurDir == "" {
   148  			break
   149  		}
   150  		curDir = newCurDir
   151  	}
   152  
   153  	r.log.Infof("Config search paths: %s", configSearchPaths)
   154  	viper.SetConfigName(".golangci")
   155  	for _, p := range configSearchPaths {
   156  		viper.AddConfigPath(p)
   157  	}
   158  }
   159  
   160  var errConfigDisabled = errors.New("config is disabled by --no-config")
   161  
   162  func (r *FileReader) parseConfigOption() (string, []string, error) {
   163  	// We use another pflag.FlagSet here to not set `changed` flag
   164  	// on cmd.Flags() options. Otherwise string slice options will be duplicated.
   165  	fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError)
   166  
   167  	var cfg Config
   168  	r.flagSetInit(fs, &cfg)
   169  
   170  	fs.Usage = func() {} // otherwise help text will be printed twice
   171  	if err := fs.Parse(os.Args); err != nil {
   172  		if err == pflag.ErrHelp {
   173  			return "", nil, err
   174  		}
   175  
   176  		return "", nil, fmt.Errorf("can't parse args: %s", err)
   177  	}
   178  
   179  	// for `-v` to work until running of preRun function
   180  	logutils.SetupVerboseLog(r.log, cfg.Run.IsVerbose)
   181  
   182  	configFile := cfg.Run.Config
   183  	if cfg.Run.NoConfig && configFile != "" {
   184  		return "", nil, fmt.Errorf("can't combine option --config and --no-config")
   185  	}
   186  
   187  	if cfg.Run.NoConfig {
   188  		return "", nil, errConfigDisabled
   189  	}
   190  
   191  	return configFile, fs.Args(), nil
   192  }