github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/config/config.go (about) 1 package config 2 3 import ( 4 "bytes" 5 "fmt" 6 "go/ast" 7 "go/token" 8 "os" 9 "path/filepath" 10 "reflect" 11 "strings" 12 13 "github.com/BurntSushi/toml" 14 "golang.org/x/tools/go/analysis" 15 ) 16 17 // Dir looks at a list of absolute file names, which should make up a 18 // single package, and returns the path of the directory that may 19 // contain a staticcheck.conf file. It returns the empty string if no 20 // such directory could be determined, for example because all files 21 // were located in Go's build cache. 22 func Dir(files []string) string { 23 if len(files) == 0 { 24 return "" 25 } 26 cache, err := os.UserCacheDir() 27 if err != nil { 28 cache = "" 29 } 30 var path string 31 for _, p := range files { 32 // FIXME(dh): using strings.HasPrefix isn't technically 33 // correct, but it should be good enough for now. 34 if cache != "" && strings.HasPrefix(p, cache) { 35 // File in the build cache of the standard Go build system 36 continue 37 } 38 path = p 39 break 40 } 41 42 if path == "" { 43 // The package only consists of generated files. 44 return "" 45 } 46 47 dir := filepath.Dir(path) 48 return dir 49 } 50 51 func dirAST(files []*ast.File, fset *token.FileSet) string { 52 names := make([]string, len(files)) 53 for i, f := range files { 54 names[i] = fset.PositionFor(f.Pos(), true).Filename 55 } 56 return Dir(names) 57 } 58 59 var Analyzer = &analysis.Analyzer{ 60 Name: "config", 61 Doc: "loads configuration for the current package tree", 62 Run: func(pass *analysis.Pass) (interface{}, error) { 63 dir := dirAST(pass.Files, pass.Fset) 64 if dir == "" { 65 cfg := DefaultConfig 66 return &cfg, nil 67 } 68 cfg, err := Load(dir) 69 if err != nil { 70 return nil, fmt.Errorf("error loading staticcheck.conf: %s", err) 71 } 72 return &cfg, nil 73 }, 74 RunDespiteErrors: true, 75 ResultType: reflect.TypeOf((*Config)(nil)), 76 } 77 78 func For(pass *analysis.Pass) *Config { 79 return pass.ResultOf[Analyzer].(*Config) 80 } 81 82 func mergeLists(a, b []string) []string { 83 out := make([]string, 0, len(a)+len(b)) 84 for _, el := range b { 85 if el == "inherit" { 86 out = append(out, a...) 87 } else { 88 out = append(out, el) 89 } 90 } 91 92 return out 93 } 94 95 func normalizeList(list []string) []string { 96 if len(list) > 1 { 97 nlist := make([]string, 0, len(list)) 98 nlist = append(nlist, list[0]) 99 for i, el := range list[1:] { 100 if el != list[i] { 101 nlist = append(nlist, el) 102 } 103 } 104 list = nlist 105 } 106 107 for _, el := range list { 108 if el == "inherit" { 109 // This should never happen, because the default config 110 // should not use "inherit" 111 panic(`unresolved "inherit"`) 112 } 113 } 114 115 return list 116 } 117 118 func (cfg Config) Merge(ocfg Config) Config { 119 if ocfg.Checks != nil { 120 cfg.Checks = mergeLists(cfg.Checks, ocfg.Checks) 121 } 122 if ocfg.Initialisms != nil { 123 cfg.Initialisms = mergeLists(cfg.Initialisms, ocfg.Initialisms) 124 } 125 if ocfg.DotImportWhitelist != nil { 126 cfg.DotImportWhitelist = mergeLists(cfg.DotImportWhitelist, ocfg.DotImportWhitelist) 127 } 128 if ocfg.HTTPStatusCodeWhitelist != nil { 129 cfg.HTTPStatusCodeWhitelist = mergeLists(cfg.HTTPStatusCodeWhitelist, ocfg.HTTPStatusCodeWhitelist) 130 } 131 return cfg 132 } 133 134 type Config struct { 135 // TODO(dh): this implementation makes it impossible for external 136 // clients to add their own checkers with configuration. At the 137 // moment, we don't really care about that; we don't encourage 138 // that people use this package. In the future, we may. The 139 // obvious solution would be using map[string]interface{}, but 140 // that's obviously subpar. 141 142 Checks []string `toml:"checks"` 143 Initialisms []string `toml:"initialisms"` 144 DotImportWhitelist []string `toml:"dot_import_whitelist"` 145 HTTPStatusCodeWhitelist []string `toml:"http_status_code_whitelist"` 146 } 147 148 func (c Config) String() string { 149 buf := &bytes.Buffer{} 150 151 fmt.Fprintf(buf, "Checks: %#v\n", c.Checks) 152 fmt.Fprintf(buf, "Initialisms: %#v\n", c.Initialisms) 153 fmt.Fprintf(buf, "DotImportWhitelist: %#v\n", c.DotImportWhitelist) 154 fmt.Fprintf(buf, "HTTPStatusCodeWhitelist: %#v", c.HTTPStatusCodeWhitelist) 155 156 return buf.String() 157 } 158 159 // DefaultConfig is the default configuration. 160 // Its initial value describes the majority of the default configuration, 161 // but the Checks field can be updated at runtime based on the analyzers being used, to disable non-default checks. 162 // For cmd/staticcheck, this is handled by (*lintcmd.Command).Run. 163 // 164 // Note that DefaultConfig shouldn't be modified while analyzers are executing. 165 var DefaultConfig = Config{ 166 Checks: []string{"all"}, 167 Initialisms: []string{ 168 "ACL", "API", "ASCII", "CPU", "CSS", "DNS", 169 "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", 170 "IP", "JSON", "QPS", "RAM", "RPC", "SLA", 171 "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", 172 "UDP", "UI", "GID", "UID", "UUID", "URI", 173 "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", 174 "XSS", "SIP", "RTP", "AMQP", "DB", "TS", 175 }, 176 DotImportWhitelist: []string{ 177 "github.com/mmcloughlin/avo/build", 178 "github.com/mmcloughlin/avo/operand", 179 "github.com/mmcloughlin/avo/reg", 180 }, 181 HTTPStatusCodeWhitelist: []string{"200", "400", "404", "500"}, 182 } 183 184 const ConfigName = "staticcheck.conf" 185 186 type ParseError struct { 187 Filename string 188 toml.ParseError 189 } 190 191 func parseConfigs(dir string) ([]Config, error) { 192 var out []Config 193 194 // TODO(dh): consider stopping at the GOPATH/module boundary 195 for dir != "" { 196 f, err := os.Open(filepath.Join(dir, ConfigName)) 197 if os.IsNotExist(err) { 198 ndir := filepath.Dir(dir) 199 if ndir == dir { 200 break 201 } 202 dir = ndir 203 continue 204 } 205 if err != nil { 206 return nil, err 207 } 208 var cfg Config 209 _, err = toml.NewDecoder(f).Decode(&cfg) 210 f.Close() 211 if err != nil { 212 if err, ok := err.(toml.ParseError); ok { 213 return nil, ParseError{ 214 Filename: filepath.Join(dir, ConfigName), 215 ParseError: err, 216 } 217 } 218 return nil, err 219 } 220 out = append(out, cfg) 221 ndir := filepath.Dir(dir) 222 if ndir == dir { 223 break 224 } 225 dir = ndir 226 } 227 out = append(out, DefaultConfig) 228 if len(out) < 2 { 229 return out, nil 230 } 231 for i := 0; i < len(out)/2; i++ { 232 out[i], out[len(out)-1-i] = out[len(out)-1-i], out[i] 233 } 234 return out, nil 235 } 236 237 func mergeConfigs(confs []Config) Config { 238 if len(confs) == 0 { 239 // This shouldn't happen because we always have at least a 240 // default config. 241 panic("trying to merge zero configs") 242 } 243 if len(confs) == 1 { 244 return confs[0] 245 } 246 conf := confs[0] 247 for _, oconf := range confs[1:] { 248 conf = conf.Merge(oconf) 249 } 250 return conf 251 } 252 253 func Load(dir string) (Config, error) { 254 confs, err := parseConfigs(dir) 255 if err != nil { 256 return Config{}, err 257 } 258 conf := mergeConfigs(confs) 259 260 conf.Checks = normalizeList(conf.Checks) 261 conf.Initialisms = normalizeList(conf.Initialisms) 262 conf.DotImportWhitelist = normalizeList(conf.DotImportWhitelist) 263 conf.HTTPStatusCodeWhitelist = normalizeList(conf.HTTPStatusCodeWhitelist) 264 265 return conf, nil 266 }