github.com/shulhan/golangci-lint@v1.10.1/pkg/config/reader.go (about) 1 package config 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path/filepath" 8 9 "github.com/golangci/golangci-lint/pkg/fsutils" 10 "github.com/golangci/golangci-lint/pkg/logutils" 11 "github.com/spf13/pflag" 12 "github.com/spf13/viper" 13 ) 14 15 type FlagSetInit func(fs *pflag.FlagSet, cfg *Config) 16 17 type FileReader struct { 18 log logutils.Log 19 cfg *Config 20 flagSetInit FlagSetInit 21 } 22 23 func NewFileReader(toCfg *Config, log logutils.Log, flagSetInit FlagSetInit) *FileReader { 24 return &FileReader{ 25 log: log, 26 cfg: toCfg, 27 flagSetInit: flagSetInit, 28 } 29 } 30 31 func (r *FileReader) Read() error { 32 // XXX: hack with double parsing for 2 purposes: 33 // 1. to access "config" option here. 34 // 2. to give config less priority than command line. 35 36 configFile, restArgs, err := r.parseConfigOption() 37 if err != nil { 38 if err == errConfigDisabled || err == pflag.ErrHelp { 39 return nil 40 } 41 42 return fmt.Errorf("can't parse --config option: %s", err) 43 } 44 45 if configFile != "" { 46 viper.SetConfigFile(configFile) 47 } else { 48 r.setupConfigFileSearch(restArgs) 49 } 50 51 return r.parseConfig() 52 } 53 54 func (r *FileReader) parseConfig() error { 55 if err := viper.ReadInConfig(); err != nil { 56 if _, ok := err.(viper.ConfigFileNotFoundError); ok { 57 return nil 58 } 59 60 return fmt.Errorf("can't read viper config: %s", err) 61 } 62 63 usedConfigFile := viper.ConfigFileUsed() 64 if usedConfigFile == "" { 65 return nil 66 } 67 68 usedConfigFile, err := fsutils.ShortestRelPath(usedConfigFile, "") 69 if err != nil { 70 r.log.Warnf("Can't pretty print config file path: %s", err) 71 } 72 r.log.Infof("Used config file %s", usedConfigFile) 73 74 if err := viper.Unmarshal(r.cfg); err != nil { 75 return fmt.Errorf("can't unmarshal config by viper: %s", err) 76 } 77 78 if err := r.validateConfig(); err != nil { 79 return fmt.Errorf("can't validate config: %s", err) 80 } 81 82 if r.cfg.InternalTest { // just for testing purposes: to detect config file usage 83 fmt.Fprintln(logutils.StdOut, "test") 84 os.Exit(0) 85 } 86 87 return nil 88 } 89 90 func (r *FileReader) validateConfig() error { 91 c := r.cfg 92 if len(c.Run.Args) != 0 { 93 return errors.New("option run.args in config isn't supported now") 94 } 95 96 if c.Run.CPUProfilePath != "" { 97 return errors.New("option run.cpuprofilepath in config isn't allowed") 98 } 99 100 if c.Run.MemProfilePath != "" { 101 return errors.New("option run.memprofilepath in config isn't allowed") 102 } 103 104 if c.Run.IsVerbose { 105 return errors.New("can't set run.verbose option with config: only on command-line") 106 } 107 108 return nil 109 } 110 111 func (r *FileReader) setupConfigFileSearch(args []string) { 112 // skip all args ([golangci-lint, run/linters]) before files/dirs list 113 for len(args) != 0 { 114 if args[0] == "run" { 115 args = args[1:] 116 break 117 } 118 119 args = args[1:] 120 } 121 122 // find first file/dir arg 123 firstArg := "./..." 124 if len(args) != 0 { 125 firstArg = args[0] 126 } 127 128 absStartPath, err := filepath.Abs(firstArg) 129 if err != nil { 130 r.log.Warnf("Can't make abs path for %q: %s", firstArg, err) 131 absStartPath = filepath.Clean(firstArg) 132 } 133 134 // start from it 135 var curDir string 136 if fsutils.IsDir(absStartPath) { 137 curDir = absStartPath 138 } else { 139 curDir = filepath.Dir(absStartPath) 140 } 141 142 // find all dirs from it up to the root 143 configSearchPaths := []string{"./"} 144 for { 145 configSearchPaths = append(configSearchPaths, curDir) 146 newCurDir := filepath.Dir(curDir) 147 if curDir == newCurDir || newCurDir == "" { 148 break 149 } 150 curDir = newCurDir 151 } 152 153 r.log.Infof("Config search paths: %s", configSearchPaths) 154 viper.SetConfigName(".golangci") 155 for _, p := range configSearchPaths { 156 viper.AddConfigPath(p) 157 } 158 } 159 160 var errConfigDisabled = errors.New("config is disabled by --no-config") 161 162 func (r *FileReader) parseConfigOption() (string, []string, error) { 163 // We use another pflag.FlagSet here to not set `changed` flag 164 // on cmd.Flags() options. Otherwise string slice options will be duplicated. 165 fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError) 166 167 var cfg Config 168 r.flagSetInit(fs, &cfg) 169 170 fs.Usage = func() {} // otherwise help text will be printed twice 171 if err := fs.Parse(os.Args); err != nil { 172 if err == pflag.ErrHelp { 173 return "", nil, err 174 } 175 176 return "", nil, fmt.Errorf("can't parse args: %s", err) 177 } 178 179 // for `-v` to work until running of preRun function 180 logutils.SetupVerboseLog(r.log, cfg.Run.IsVerbose) 181 182 configFile := cfg.Run.Config 183 if cfg.Run.NoConfig && configFile != "" { 184 return "", nil, fmt.Errorf("can't combine option --config and --no-config") 185 } 186 187 if cfg.Run.NoConfig { 188 return "", nil, errConfigDisabled 189 } 190 191 return configFile, fs.Args(), nil 192 }