Skip to content

Commit 973f628

Browse files
committed
chore: neutral configuration loader
1 parent c3a7802 commit 973f628

File tree

3 files changed

+243
-188
lines changed

3 files changed

+243
-188
lines changed

pkg/config/base_loader.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package config
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"slices"
9+
10+
"github.com/go-viper/mapstructure/v2"
11+
"github.com/mitchellh/go-homedir"
12+
"github.com/spf13/viper"
13+
14+
"github.com/golangci/golangci-lint/pkg/exitcodes"
15+
"github.com/golangci/golangci-lint/pkg/fsutils"
16+
"github.com/golangci/golangci-lint/pkg/logutils"
17+
)
18+
19+
type CFG interface {
20+
IsInternalTest() bool
21+
SetConfigDir(dir string)
22+
}
23+
24+
type BaseLoader struct {
25+
opts LoaderOptions
26+
27+
viper *viper.Viper
28+
29+
log logutils.Log
30+
31+
cfg CFG
32+
args []string
33+
}
34+
35+
func NewBaseLoader(log logutils.Log, v *viper.Viper, opts LoaderOptions, cfg CFG, args []string) *BaseLoader {
36+
return &BaseLoader{
37+
opts: opts,
38+
viper: v,
39+
log: log,
40+
cfg: cfg,
41+
args: args,
42+
}
43+
}
44+
45+
func (l *BaseLoader) Load() error {
46+
err := l.setConfigFile()
47+
if err != nil {
48+
return err
49+
}
50+
51+
err = l.parseConfig()
52+
if err != nil {
53+
return err
54+
}
55+
56+
return nil
57+
}
58+
59+
func (l *BaseLoader) setConfigFile() error {
60+
configFile, err := l.evaluateOptions()
61+
if err != nil {
62+
if errors.Is(err, errConfigDisabled) {
63+
return nil
64+
}
65+
66+
return fmt.Errorf("can't parse --config option: %w", err)
67+
}
68+
69+
if configFile != "" {
70+
l.viper.SetConfigFile(configFile)
71+
72+
// Assume YAML if the file has no extension.
73+
if filepath.Ext(configFile) == "" {
74+
l.viper.SetConfigType("yaml")
75+
}
76+
} else {
77+
l.setupConfigFileSearch()
78+
}
79+
80+
return nil
81+
}
82+
83+
func (l *BaseLoader) evaluateOptions() (string, error) {
84+
if l.opts.NoConfig && l.opts.Config != "" {
85+
return "", errors.New("can't combine option --config and --no-config")
86+
}
87+
88+
if l.opts.NoConfig {
89+
return "", errConfigDisabled
90+
}
91+
92+
configFile, err := homedir.Expand(l.opts.Config)
93+
if err != nil {
94+
return "", errors.New("failed to expand configuration path")
95+
}
96+
97+
return configFile, nil
98+
}
99+
100+
func (l *BaseLoader) setupConfigFileSearch() {
101+
l.viper.SetConfigName(".golangci")
102+
103+
configSearchPaths := l.getConfigSearchPaths()
104+
105+
l.log.Infof("Config search paths: %s", configSearchPaths)
106+
107+
for _, p := range configSearchPaths {
108+
l.viper.AddConfigPath(p)
109+
}
110+
}
111+
112+
func (l *BaseLoader) getConfigSearchPaths() []string {
113+
firstArg := "./..."
114+
if len(l.args) > 0 {
115+
firstArg = l.args[0]
116+
}
117+
118+
absPath, err := filepath.Abs(firstArg)
119+
if err != nil {
120+
l.log.Warnf("Can't make abs path for %q: %s", firstArg, err)
121+
absPath = filepath.Clean(firstArg)
122+
}
123+
124+
// start from it
125+
var currentDir string
126+
if fsutils.IsDir(absPath) {
127+
currentDir = absPath
128+
} else {
129+
currentDir = filepath.Dir(absPath)
130+
}
131+
132+
// find all dirs from it up to the root
133+
searchPaths := []string{"./"}
134+
135+
for {
136+
searchPaths = append(searchPaths, currentDir)
137+
138+
parent := filepath.Dir(currentDir)
139+
if currentDir == parent || parent == "" {
140+
break
141+
}
142+
143+
currentDir = parent
144+
}
145+
146+
// find home directory for global config
147+
if home, err := homedir.Dir(); err != nil {
148+
l.log.Warnf("Can't get user's home directory: %v", err)
149+
} else if !slices.Contains(searchPaths, home) {
150+
searchPaths = append(searchPaths, home)
151+
}
152+
153+
return searchPaths
154+
}
155+
156+
func (l *BaseLoader) parseConfig() error {
157+
if err := l.viper.ReadInConfig(); err != nil {
158+
var configFileNotFoundError viper.ConfigFileNotFoundError
159+
if errors.As(err, &configFileNotFoundError) {
160+
// Load configuration from flags only.
161+
err = l.viper.Unmarshal(l.cfg, customDecoderHook())
162+
if err != nil {
163+
return fmt.Errorf("can't unmarshal config by viper (flags): %w", err)
164+
}
165+
166+
return nil
167+
}
168+
169+
return fmt.Errorf("can't read viper config: %w", err)
170+
}
171+
172+
err := l.setConfigDir()
173+
if err != nil {
174+
return err
175+
}
176+
177+
// Load configuration from all sources (flags, file).
178+
if err := l.viper.Unmarshal(l.cfg, customDecoderHook()); err != nil {
179+
return fmt.Errorf("can't unmarshal config by viper (flags, file): %w", err)
180+
}
181+
182+
if l.cfg.IsInternalTest() { // just for testing purposes: to detect config file usage
183+
_, _ = fmt.Fprintln(logutils.StdOut, "test")
184+
os.Exit(exitcodes.Success)
185+
}
186+
187+
return nil
188+
}
189+
190+
func (l *BaseLoader) setConfigDir() error {
191+
usedConfigFile := l.viper.ConfigFileUsed()
192+
if usedConfigFile == "" {
193+
return nil
194+
}
195+
196+
if usedConfigFile == os.Stdin.Name() {
197+
usedConfigFile = ""
198+
l.log.Infof("Reading config file stdin")
199+
} else {
200+
var err error
201+
usedConfigFile, err = fsutils.ShortestRelPath(usedConfigFile, "")
202+
if err != nil {
203+
l.log.Warnf("Can't pretty print config file path: %v", err)
204+
}
205+
206+
l.log.Infof("Used config file %s", usedConfigFile)
207+
}
208+
209+
usedConfigDir, err := filepath.Abs(filepath.Dir(usedConfigFile))
210+
if err != nil {
211+
return errors.New("can't get config directory")
212+
}
213+
214+
l.cfg.SetConfigDir(usedConfigDir)
215+
216+
return nil
217+
}
218+
219+
func customDecoderHook() viper.DecoderConfigOption {
220+
return viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
221+
// Default hooks (https://github.com/spf13/viper/blob/518241257478c557633ab36e474dfcaeb9a3c623/viper.go#L135-L138).
222+
mapstructure.StringToTimeDurationHookFunc(),
223+
mapstructure.StringToSliceHookFunc(","),
224+
225+
// Needed for forbidigo, and output.formats.
226+
mapstructure.TextUnmarshallerHookFunc(),
227+
))
228+
}

pkg/config/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,19 @@ func (c *Config) GetConfigDir() string {
4646
return c.cfgDir
4747
}
4848

49+
// SetConfigDir sets the path to directory that contains golangci-lint config file.
50+
func (c *Config) SetConfigDir(dir string) {
51+
c.cfgDir = dir
52+
}
53+
4954
func (c *Config) GetBasePath() string {
5055
return c.basePath
5156
}
5257

58+
func (c *Config) IsInternalTest() bool {
59+
return c.InternalTest
60+
}
61+
5362
func (c *Config) Validate() error {
5463
validators := []func() error{
5564
c.Run.Validate,

0 commit comments

Comments
 (0)