github.com/nozzle/golangci-lint@v1.49.0-nz3/pkg/config/reader.go (about)

     1  package config
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/mitchellh/go-homedir"
    11  	"github.com/spf13/viper"
    12  
    13  	"github.com/golangci/golangci-lint/pkg/exitcodes"
    14  	"github.com/golangci/golangci-lint/pkg/fsutils"
    15  	"github.com/golangci/golangci-lint/pkg/logutils"
    16  	"github.com/golangci/golangci-lint/pkg/sliceutil"
    17  )
    18  
    19  type FileReader struct {
    20  	log            logutils.Log
    21  	cfg            *Config
    22  	commandLineCfg *Config
    23  }
    24  
    25  func NewFileReader(toCfg, commandLineCfg *Config, log logutils.Log) *FileReader {
    26  	return &FileReader{
    27  		log:            log,
    28  		cfg:            toCfg,
    29  		commandLineCfg: commandLineCfg,
    30  	}
    31  }
    32  
    33  func (r *FileReader) Read() error {
    34  	// XXX: hack with double parsing for 2 purposes:
    35  	// 1. to access "config" option here.
    36  	// 2. to give config less priority than command line.
    37  
    38  	configFile, err := r.parseConfigOption()
    39  	if err != nil {
    40  		if err == errConfigDisabled {
    41  			return nil
    42  		}
    43  
    44  		return fmt.Errorf("can't parse --config option: %s", err)
    45  	}
    46  
    47  	if configFile != "" {
    48  		viper.SetConfigFile(configFile)
    49  
    50  		// Assume YAML if the file has no extension.
    51  		if filepath.Ext(configFile) == "" {
    52  			viper.SetConfigType("yaml")
    53  		}
    54  	} else {
    55  		r.setupConfigFileSearch()
    56  	}
    57  
    58  	return r.parseConfig()
    59  }
    60  
    61  func (r *FileReader) parseConfig() error {
    62  	if err := viper.ReadInConfig(); err != nil {
    63  		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
    64  			return nil
    65  		}
    66  
    67  		return fmt.Errorf("can't read viper config: %s", err)
    68  	}
    69  
    70  	usedConfigFile := viper.ConfigFileUsed()
    71  	if usedConfigFile == "" {
    72  		return nil
    73  	}
    74  
    75  	usedConfigFile, err := fsutils.ShortestRelPath(usedConfigFile, "")
    76  	if err != nil {
    77  		r.log.Warnf("Can't pretty print config file path: %s", err)
    78  	}
    79  	r.log.Infof("Used config file %s", usedConfigFile)
    80  	usedConfigDir := filepath.Dir(usedConfigFile)
    81  	if usedConfigDir, err = filepath.Abs(usedConfigDir); err != nil {
    82  		return errors.New("can't get config directory")
    83  	}
    84  	r.cfg.cfgDir = usedConfigDir
    85  
    86  	if err := viper.Unmarshal(r.cfg); err != nil {
    87  		return fmt.Errorf("can't unmarshal config by viper: %s", err)
    88  	}
    89  
    90  	if err := r.validateConfig(); err != nil {
    91  		return fmt.Errorf("can't validate config: %s", err)
    92  	}
    93  
    94  	if r.cfg.InternalTest { // just for testing purposes: to detect config file usage
    95  		fmt.Fprintln(logutils.StdOut, "test")
    96  		os.Exit(exitcodes.Success)
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  func (r *FileReader) validateConfig() error {
   103  	c := r.cfg
   104  	if len(c.Run.Args) != 0 {
   105  		return errors.New("option run.args in config isn't supported now")
   106  	}
   107  
   108  	if c.Run.CPUProfilePath != "" {
   109  		return errors.New("option run.cpuprofilepath in config isn't allowed")
   110  	}
   111  
   112  	if c.Run.MemProfilePath != "" {
   113  		return errors.New("option run.memprofilepath in config isn't allowed")
   114  	}
   115  
   116  	if c.Run.TracePath != "" {
   117  		return errors.New("option run.tracepath in config isn't allowed")
   118  	}
   119  
   120  	if c.Run.IsVerbose {
   121  		return errors.New("can't set run.verbose option with config: only on command-line")
   122  	}
   123  	for i, rule := range c.Issues.ExcludeRules {
   124  		if err := rule.Validate(); err != nil {
   125  			return fmt.Errorf("error in exclude rule #%d: %v", i, err)
   126  		}
   127  	}
   128  	if len(c.Severity.Rules) > 0 && c.Severity.Default == "" {
   129  		return errors.New("can't set severity rule option: no default severity defined")
   130  	}
   131  	for i, rule := range c.Severity.Rules {
   132  		if err := rule.Validate(); err != nil {
   133  			return fmt.Errorf("error in severity rule #%d: %v", i, err)
   134  		}
   135  	}
   136  	if err := c.LintersSettings.Govet.Validate(); err != nil {
   137  		return fmt.Errorf("error in govet config: %v", err)
   138  	}
   139  	return nil
   140  }
   141  
   142  func getFirstPathArg() string {
   143  	args := os.Args
   144  
   145  	// skip all args ([golangci-lint, run/linters]) before files/dirs list
   146  	for len(args) != 0 {
   147  		if args[0] == "run" {
   148  			args = args[1:]
   149  			break
   150  		}
   151  
   152  		args = args[1:]
   153  	}
   154  
   155  	// find first file/dir arg
   156  	firstArg := "./..."
   157  	for _, arg := range args {
   158  		if !strings.HasPrefix(arg, "-") {
   159  			firstArg = arg
   160  			break
   161  		}
   162  	}
   163  
   164  	return firstArg
   165  }
   166  
   167  func (r *FileReader) setupConfigFileSearch() {
   168  	firstArg := getFirstPathArg()
   169  	absStartPath, err := filepath.Abs(firstArg)
   170  	if err != nil {
   171  		r.log.Warnf("Can't make abs path for %q: %s", firstArg, err)
   172  		absStartPath = filepath.Clean(firstArg)
   173  	}
   174  
   175  	// start from it
   176  	var curDir string
   177  	if fsutils.IsDir(absStartPath) {
   178  		curDir = absStartPath
   179  	} else {
   180  		curDir = filepath.Dir(absStartPath)
   181  	}
   182  
   183  	// find all dirs from it up to the root
   184  	configSearchPaths := []string{"./"}
   185  
   186  	for {
   187  		configSearchPaths = append(configSearchPaths, curDir)
   188  		newCurDir := filepath.Dir(curDir)
   189  		if curDir == newCurDir || newCurDir == "" {
   190  			break
   191  		}
   192  		curDir = newCurDir
   193  	}
   194  
   195  	// find home directory for global config
   196  	if home, err := homedir.Dir(); err != nil {
   197  		r.log.Warnf("Can't get user's home directory: %s", err.Error())
   198  	} else if !sliceutil.Contains(configSearchPaths, home) {
   199  		configSearchPaths = append(configSearchPaths, home)
   200  	}
   201  
   202  	r.log.Infof("Config search paths: %s", configSearchPaths)
   203  	viper.SetConfigName(".golangci")
   204  	for _, p := range configSearchPaths {
   205  		viper.AddConfigPath(p)
   206  	}
   207  }
   208  
   209  var errConfigDisabled = errors.New("config is disabled by --no-config")
   210  
   211  func (r *FileReader) parseConfigOption() (string, error) {
   212  	cfg := r.commandLineCfg
   213  	if cfg == nil {
   214  		return "", nil
   215  	}
   216  
   217  	configFile := cfg.Run.Config
   218  	if cfg.Run.NoConfig && configFile != "" {
   219  		return "", errors.New("can't combine option --config and --no-config")
   220  	}
   221  
   222  	if cfg.Run.NoConfig {
   223  		return "", errConfigDisabled
   224  	}
   225  
   226  	configFile, err := homedir.Expand(configFile)
   227  	if err != nil {
   228  		return "", errors.New("failed to expand configuration path")
   229  	}
   230  
   231  	return configFile, nil
   232  }