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