452 lines
12 KiB
Go
452 lines
12 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
|
|
"github.com/go-viper/mapstructure/v2"
|
|
"github.com/mitchellh/go-homedir"
|
|
"github.com/spf13/pflag"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/golangci/golangci-lint/pkg/exitcodes"
|
|
"github.com/golangci/golangci-lint/pkg/fsutils"
|
|
"github.com/golangci/golangci-lint/pkg/logutils"
|
|
)
|
|
|
|
var errConfigDisabled = errors.New("config is disabled by --no-config")
|
|
|
|
type LoaderOptions struct {
|
|
Config string // Flag only. The path to the golangci config file, as specified with the --config argument.
|
|
NoConfig bool // Flag only.
|
|
}
|
|
|
|
type LoadOptions struct {
|
|
CheckDeprecation bool
|
|
Validation bool
|
|
}
|
|
|
|
type Loader struct {
|
|
opts LoaderOptions
|
|
|
|
viper *viper.Viper
|
|
fs *pflag.FlagSet
|
|
|
|
log logutils.Log
|
|
|
|
cfg *Config
|
|
args []string
|
|
}
|
|
|
|
func NewLoader(log logutils.Log, v *viper.Viper, fs *pflag.FlagSet, opts LoaderOptions, cfg *Config, args []string) *Loader {
|
|
return &Loader{
|
|
opts: opts,
|
|
viper: v,
|
|
fs: fs,
|
|
log: log,
|
|
cfg: cfg,
|
|
args: args,
|
|
}
|
|
}
|
|
|
|
func (l *Loader) Load(opts LoadOptions) error {
|
|
err := l.setConfigFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = l.parseConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
l.applyStringSliceHack()
|
|
|
|
if opts.CheckDeprecation {
|
|
err = l.handleDeprecation()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
l.handleGoVersion()
|
|
|
|
err = l.handleEnableOnlyOption()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.Validation {
|
|
err = l.cfg.Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *Loader) setConfigFile() error {
|
|
configFile, err := l.evaluateOptions()
|
|
if err != nil {
|
|
if errors.Is(err, errConfigDisabled) {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("can't parse --config option: %w", err)
|
|
}
|
|
|
|
if configFile != "" {
|
|
l.viper.SetConfigFile(configFile)
|
|
|
|
// Assume YAML if the file has no extension.
|
|
if filepath.Ext(configFile) == "" {
|
|
l.viper.SetConfigType("yaml")
|
|
}
|
|
} else {
|
|
l.setupConfigFileSearch()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *Loader) evaluateOptions() (string, error) {
|
|
if l.opts.NoConfig && l.opts.Config != "" {
|
|
return "", errors.New("can't combine option --config and --no-config")
|
|
}
|
|
|
|
if l.opts.NoConfig {
|
|
return "", errConfigDisabled
|
|
}
|
|
|
|
configFile, err := homedir.Expand(l.opts.Config)
|
|
if err != nil {
|
|
return "", errors.New("failed to expand configuration path")
|
|
}
|
|
|
|
return configFile, nil
|
|
}
|
|
|
|
func (l *Loader) setupConfigFileSearch() {
|
|
l.viper.SetConfigName(".golangci")
|
|
|
|
configSearchPaths := l.getConfigSearchPaths()
|
|
|
|
l.log.Infof("Config search paths: %s", configSearchPaths)
|
|
|
|
for _, p := range configSearchPaths {
|
|
l.viper.AddConfigPath(p)
|
|
}
|
|
}
|
|
|
|
func (l *Loader) getConfigSearchPaths() []string {
|
|
firstArg := "./..."
|
|
if len(l.args) > 0 {
|
|
firstArg = l.args[0]
|
|
}
|
|
|
|
absPath, err := filepath.Abs(firstArg)
|
|
if err != nil {
|
|
l.log.Warnf("Can't make abs path for %q: %s", firstArg, err)
|
|
absPath = filepath.Clean(firstArg)
|
|
}
|
|
|
|
// start from it
|
|
var currentDir string
|
|
if fsutils.IsDir(absPath) {
|
|
currentDir = absPath
|
|
} else {
|
|
currentDir = filepath.Dir(absPath)
|
|
}
|
|
|
|
// find all dirs from it up to the root
|
|
searchPaths := []string{"./"}
|
|
|
|
for {
|
|
searchPaths = append(searchPaths, currentDir)
|
|
|
|
parent := filepath.Dir(currentDir)
|
|
if currentDir == parent || parent == "" {
|
|
break
|
|
}
|
|
|
|
currentDir = parent
|
|
}
|
|
|
|
// find home directory for global config
|
|
if home, err := homedir.Dir(); err != nil {
|
|
l.log.Warnf("Can't get user's home directory: %v", err)
|
|
} else if !slices.Contains(searchPaths, home) {
|
|
searchPaths = append(searchPaths, home)
|
|
}
|
|
|
|
return searchPaths
|
|
}
|
|
|
|
func (l *Loader) parseConfig() error {
|
|
if err := l.viper.ReadInConfig(); err != nil {
|
|
var configFileNotFoundError viper.ConfigFileNotFoundError
|
|
if errors.As(err, &configFileNotFoundError) {
|
|
// Load configuration from flags only.
|
|
err = l.viper.Unmarshal(l.cfg, customDecoderHook())
|
|
if err != nil {
|
|
return fmt.Errorf("can't unmarshal config by viper (flags): %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("can't read viper config: %w", err)
|
|
}
|
|
|
|
err := l.setConfigDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load configuration from all sources (flags, file).
|
|
if err := l.viper.Unmarshal(l.cfg, customDecoderHook()); err != nil {
|
|
return fmt.Errorf("can't unmarshal config by viper (flags, file): %w", err)
|
|
}
|
|
|
|
if l.cfg.InternalTest { // just for testing purposes: to detect config file usage
|
|
_, _ = fmt.Fprintln(logutils.StdOut, "test")
|
|
os.Exit(exitcodes.Success)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *Loader) setConfigDir() error {
|
|
usedConfigFile := l.viper.ConfigFileUsed()
|
|
if usedConfigFile == "" {
|
|
return nil
|
|
}
|
|
|
|
if usedConfigFile == os.Stdin.Name() {
|
|
usedConfigFile = ""
|
|
l.log.Infof("Reading config file stdin")
|
|
} else {
|
|
var err error
|
|
usedConfigFile, err = fsutils.ShortestRelPath(usedConfigFile, "")
|
|
if err != nil {
|
|
l.log.Warnf("Can't pretty print config file path: %v", err)
|
|
}
|
|
|
|
l.log.Infof("Used config file %s", usedConfigFile)
|
|
}
|
|
|
|
usedConfigDir, err := filepath.Abs(filepath.Dir(usedConfigFile))
|
|
if err != nil {
|
|
return errors.New("can't get config directory")
|
|
}
|
|
|
|
l.cfg.cfgDir = usedConfigDir
|
|
|
|
return nil
|
|
}
|
|
|
|
// Hack to append values from StringSlice flags.
|
|
// Viper always overrides StringSlice values.
|
|
// https://github.com/spf13/viper/issues/1448
|
|
// So StringSlice flags are not bind to Viper like that their values are obtain via Cobra Flags.
|
|
func (l *Loader) applyStringSliceHack() {
|
|
if l.fs == nil {
|
|
return
|
|
}
|
|
|
|
l.appendStringSlice("enable", &l.cfg.Linters.Enable)
|
|
l.appendStringSlice("disable", &l.cfg.Linters.Disable)
|
|
l.appendStringSlice("presets", &l.cfg.Linters.Presets)
|
|
l.appendStringSlice("build-tags", &l.cfg.Run.BuildTags)
|
|
l.appendStringSlice("exclude", &l.cfg.Issues.ExcludePatterns)
|
|
|
|
l.appendStringSlice("skip-dirs", &l.cfg.Run.SkipDirs)
|
|
l.appendStringSlice("skip-files", &l.cfg.Run.SkipFiles)
|
|
l.appendStringSlice("exclude-dirs", &l.cfg.Issues.ExcludeDirs)
|
|
l.appendStringSlice("exclude-files", &l.cfg.Issues.ExcludeFiles)
|
|
}
|
|
|
|
func (l *Loader) appendStringSlice(name string, current *[]string) {
|
|
if l.fs.Changed(name) {
|
|
val, _ := l.fs.GetStringSlice(name)
|
|
*current = append(*current, val...)
|
|
}
|
|
}
|
|
|
|
func (l *Loader) handleGoVersion() {
|
|
if l.cfg.Run.Go == "" {
|
|
l.cfg.Run.Go = detectGoVersion()
|
|
}
|
|
|
|
l.cfg.LintersSettings.Govet.Go = l.cfg.Run.Go
|
|
|
|
l.cfg.LintersSettings.ParallelTest.Go = l.cfg.Run.Go
|
|
|
|
if l.cfg.LintersSettings.Gofumpt.LangVersion == "" {
|
|
l.cfg.LintersSettings.Gofumpt.LangVersion = l.cfg.Run.Go
|
|
}
|
|
|
|
trimmedGoVersion := trimGoVersion(l.cfg.Run.Go)
|
|
|
|
l.cfg.LintersSettings.Gocritic.Go = trimmedGoVersion
|
|
|
|
// staticcheck related linters.
|
|
if l.cfg.LintersSettings.Staticcheck.GoVersion == "" {
|
|
l.cfg.LintersSettings.Staticcheck.GoVersion = trimmedGoVersion
|
|
}
|
|
if l.cfg.LintersSettings.Gosimple.GoVersion == "" {
|
|
l.cfg.LintersSettings.Gosimple.GoVersion = trimmedGoVersion
|
|
}
|
|
if l.cfg.LintersSettings.Stylecheck.GoVersion == "" {
|
|
l.cfg.LintersSettings.Stylecheck.GoVersion = trimmedGoVersion
|
|
}
|
|
}
|
|
|
|
func (l *Loader) handleDeprecation() error {
|
|
if l.cfg.InternalTest || l.cfg.InternalCmdTest || os.Getenv(logutils.EnvTestRun) == "1" {
|
|
return nil
|
|
}
|
|
|
|
// Deprecated since v1.57.0
|
|
if len(l.cfg.Run.SkipFiles) > 0 {
|
|
l.log.Warnf("The configuration option `run.skip-files` is deprecated, please use `issues.exclude-files`.")
|
|
l.cfg.Issues.ExcludeFiles = l.cfg.Run.SkipFiles
|
|
}
|
|
|
|
// Deprecated since v1.57.0
|
|
if len(l.cfg.Run.SkipDirs) > 0 {
|
|
l.log.Warnf("The configuration option `run.skip-dirs` is deprecated, please use `issues.exclude-dirs`.")
|
|
l.cfg.Issues.ExcludeDirs = l.cfg.Run.SkipDirs
|
|
}
|
|
|
|
// The 2 options are true by default.
|
|
// Deprecated since v1.57.0
|
|
if !l.cfg.Run.UseDefaultSkipDirs {
|
|
l.log.Warnf("The configuration option `run.skip-dirs-use-default` is deprecated, please use `issues.exclude-dirs-use-default`.")
|
|
}
|
|
l.cfg.Issues.UseDefaultExcludeDirs = l.cfg.Run.UseDefaultSkipDirs && l.cfg.Issues.UseDefaultExcludeDirs
|
|
|
|
// The 2 options are false by default.
|
|
// Deprecated since v1.57.0
|
|
if l.cfg.Run.ShowStats {
|
|
l.log.Warnf("The configuration option `run.show-stats` is deprecated, please use `output.show-stats`")
|
|
}
|
|
l.cfg.Output.ShowStats = l.cfg.Run.ShowStats || l.cfg.Output.ShowStats
|
|
|
|
// Deprecated since v1.57.0
|
|
if l.cfg.Output.Format != "" {
|
|
l.log.Warnf("The configuration option `output.format` is deprecated, please use `output.formats`")
|
|
|
|
var f OutputFormats
|
|
err := f.UnmarshalText([]byte(l.cfg.Output.Format))
|
|
if err != nil {
|
|
return fmt.Errorf("unmarshal output format: %w", err)
|
|
}
|
|
|
|
l.cfg.Output.Formats = f
|
|
}
|
|
|
|
l.handleLinterOptionDeprecations()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *Loader) handleLinterOptionDeprecations() {
|
|
// Deprecated since v1.57.0,
|
|
// but it was unofficially deprecated since v1.19 (2019) (https://github.com/golangci/golangci-lint/pull/697).
|
|
if l.cfg.LintersSettings.Govet.CheckShadowing {
|
|
l.log.Warnf("The configuration option `linters.govet.check-shadowing` is deprecated. " +
|
|
"Please enable `shadow` instead, if you are not using `enable-all`.")
|
|
}
|
|
|
|
if l.cfg.LintersSettings.CopyLoopVar.IgnoreAlias {
|
|
l.log.Warnf("The configuration option `linters.copyloopvar.ignore-alias` is deprecated and ignored," +
|
|
"please use `linters.copyloopvar.check-alias`.")
|
|
}
|
|
|
|
// Deprecated since v1.42.0.
|
|
if l.cfg.LintersSettings.Errcheck.Exclude != "" {
|
|
l.log.Warnf("The configuration option `linters.errcheck.exclude` is deprecated, please use `linters.errcheck.exclude-functions`.")
|
|
}
|
|
|
|
// Deprecated since v1.44.0.
|
|
if l.cfg.LintersSettings.Gci.LocalPrefixes != "" {
|
|
l.log.Warnf("The configuration option `linters.gci.local-prefixes` is deprecated, please use `prefix()` inside `linters.gci.sections`.")
|
|
}
|
|
|
|
// Deprecated since v1.33.0.
|
|
if l.cfg.LintersSettings.Godot.CheckAll {
|
|
l.log.Warnf("The configuration option `linters.godot.check-all` is deprecated, please use `linters.godot.scope: all`.")
|
|
}
|
|
|
|
// Deprecated since v1.44.0.
|
|
if len(l.cfg.LintersSettings.Gomnd.Settings) > 0 {
|
|
l.log.Warnf("The configuration option `linters.gomnd.settings` is deprecated. Please use the options " +
|
|
"`linters.gomnd.checks`,`linters.gomnd.ignored-numbers`,`linters.gomnd.ignored-files`,`linters.gomnd.ignored-functions`.")
|
|
}
|
|
|
|
// Deprecated since v1.47.0
|
|
if l.cfg.LintersSettings.Gofumpt.LangVersion != "" {
|
|
l.log.Warnf("The configuration option `linters.gofumpt.lang-version` is deprecated, please use global `run.go`.")
|
|
}
|
|
|
|
// Deprecated since v1.47.0
|
|
if l.cfg.LintersSettings.Staticcheck.GoVersion != "" {
|
|
l.log.Warnf("The configuration option `linters.staticcheck.go` is deprecated, please use global `run.go`.")
|
|
}
|
|
|
|
// Deprecated since v1.47.0
|
|
if l.cfg.LintersSettings.Gosimple.GoVersion != "" {
|
|
l.log.Warnf("The configuration option `linters.gosimple.go` is deprecated, please use global `run.go`.")
|
|
}
|
|
|
|
// Deprecated since v1.47.0
|
|
if l.cfg.LintersSettings.Stylecheck.GoVersion != "" {
|
|
l.log.Warnf("The configuration option `linters.stylecheck.go` is deprecated, please use global `run.go`.")
|
|
}
|
|
|
|
// Deprecated since v1.58.0
|
|
if l.cfg.LintersSettings.SlogLint.ContextOnly {
|
|
l.log.Warnf("The configuration option `linters.sloglint.context-only` is deprecated, please use `linters.sloglint.context`")
|
|
if l.cfg.LintersSettings.SlogLint.Context == "" {
|
|
l.cfg.LintersSettings.SlogLint.Context = "all"
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *Loader) handleEnableOnlyOption() error {
|
|
lookup := l.fs.Lookup("enable-only")
|
|
if lookup == nil {
|
|
return nil
|
|
}
|
|
|
|
only, err := l.fs.GetStringSlice("enable-only")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(only) > 0 {
|
|
l.cfg.Linters = Linters{
|
|
Enable: only,
|
|
DisableAll: true,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func customDecoderHook() viper.DecoderConfigOption {
|
|
return viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
|
|
// Default hooks (https://github.com/spf13/viper/blob/518241257478c557633ab36e474dfcaeb9a3c623/viper.go#L135-L138).
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
mapstructure.StringToSliceHookFunc(","),
|
|
|
|
// Needed for forbidigo, and output.formats.
|
|
mapstructure.TextUnmarshallerHookFunc(),
|
|
))
|
|
}
|