
1. Log all warnings, don't hide none of them 2. Write fatal messages (stop analysis) with error log level 3. Remove ugly timestamp counter from logrus output 4. Print nested module prefix in log 5. Make logger abstraction: no global logging anymore 6. Refactor config reading to config.FileReader struct to avoid passing logger into every function 7. Replace exit codes hardcoding with constants in exitcodes package 8. Fail test if any warning was logged 9. Fix calculation of relative path if we analyze parent dir ../ 10. Move Runner initialization from Executor to NewRunner func 11. Log every AST parsing error 12. Properly print used config file path in verbose mode 13. Print package files if only 1 package is analyzedin verbose mode, print not compiling packages in verbose mode 14. Forbid usage of github.com/sirupsen/logrus by DepGuard linter 15. Add default ignore pattern to folint: "comment on exported const"
194 lines
4.5 KiB
Go
194 lines
4.5 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/golangci/golangci-lint/pkg/fsutils"
|
|
"github.com/golangci/golangci-lint/pkg/logutils"
|
|
"github.com/golangci/golangci-lint/pkg/printers"
|
|
"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(printers.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
|
|
}
|