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