2021-11-14 20:01:54 +00:00
|
|
|
package config
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/mitchellh/go-homedir"
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
|
2022-02-24 16:33:24 +00:00
|
|
|
"github.com/golangci/golangci-lint/pkg/exitcodes"
|
2021-11-14 20:01:54 +00:00
|
|
|
"github.com/golangci/golangci-lint/pkg/fsutils"
|
|
|
|
"github.com/golangci/golangci-lint/pkg/logutils"
|
|
|
|
"github.com/golangci/golangci-lint/pkg/sliceutil"
|
|
|
|
)
|
|
|
|
|
|
|
|
type FileReader struct {
|
|
|
|
log logutils.Log
|
|
|
|
cfg *Config
|
|
|
|
commandLineCfg *Config
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewFileReader(toCfg, commandLineCfg *Config, log logutils.Log) *FileReader {
|
|
|
|
return &FileReader{
|
|
|
|
log: log,
|
|
|
|
cfg: toCfg,
|
|
|
|
commandLineCfg: commandLineCfg,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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, err := r.parseConfigOption()
|
|
|
|
if err != nil {
|
|
|
|
if err == errConfigDisabled {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Errorf("can't parse --config option: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if configFile != "" {
|
|
|
|
viper.SetConfigFile(configFile)
|
|
|
|
} else {
|
|
|
|
r.setupConfigFileSearch()
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2021-11-16 20:07:53 +00:00
|
|
|
usedConfigDir := filepath.Dir(usedConfigFile)
|
|
|
|
if usedConfigDir, err = filepath.Abs(usedConfigDir); err != nil {
|
|
|
|
return fmt.Errorf("can't get config directory")
|
|
|
|
}
|
|
|
|
r.cfg.cfgDir = usedConfigDir
|
2021-11-14 20:01:54 +00:00
|
|
|
|
|
|
|
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")
|
2022-02-24 16:33:24 +00:00
|
|
|
os.Exit(exitcodes.Success)
|
2021-11-14 20:01:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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.TracePath != "" {
|
|
|
|
return errors.New("option run.tracepath in config isn't allowed")
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Run.IsVerbose {
|
|
|
|
return errors.New("can't set run.verbose option with config: only on command-line")
|
|
|
|
}
|
|
|
|
for i, rule := range c.Issues.ExcludeRules {
|
|
|
|
if err := rule.Validate(); err != nil {
|
|
|
|
return fmt.Errorf("error in exclude rule #%d: %v", i, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(c.Severity.Rules) > 0 && c.Severity.Default == "" {
|
|
|
|
return errors.New("can't set severity rule option: no default severity defined")
|
|
|
|
}
|
|
|
|
for i, rule := range c.Severity.Rules {
|
|
|
|
if err := rule.Validate(); err != nil {
|
|
|
|
return fmt.Errorf("error in severity rule #%d: %v", i, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := c.LintersSettings.Govet.Validate(); err != nil {
|
|
|
|
return fmt.Errorf("error in govet config: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getFirstPathArg() string {
|
|
|
|
args := os.Args
|
|
|
|
|
|
|
|
// 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 := "./..."
|
|
|
|
for _, arg := range args {
|
|
|
|
if !strings.HasPrefix(arg, "-") {
|
|
|
|
firstArg = arg
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return firstArg
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *FileReader) setupConfigFileSearch() {
|
|
|
|
firstArg := getFirstPathArg()
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// find home directory for global config
|
|
|
|
if home, err := homedir.Dir(); err != nil {
|
|
|
|
r.log.Warnf("Can't get user's home directory: %s", err.Error())
|
|
|
|
} else if !sliceutil.Contains(configSearchPaths, home) {
|
|
|
|
configSearchPaths = append(configSearchPaths, home)
|
|
|
|
}
|
|
|
|
|
|
|
|
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, error) {
|
|
|
|
cfg := r.commandLineCfg
|
|
|
|
if cfg == nil {
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
configFile := cfg.Run.Config
|
|
|
|
if cfg.Run.NoConfig && configFile != "" {
|
|
|
|
return "", fmt.Errorf("can't combine option --config and --no-config")
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.Run.NoConfig {
|
|
|
|
return "", errConfigDisabled
|
|
|
|
}
|
|
|
|
|
|
|
|
configFile, err := homedir.Expand(configFile)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("failed to expand configuration path")
|
|
|
|
}
|
|
|
|
|
|
|
|
return configFile, nil
|
|
|
|
}
|