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