config.go (7031B)
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) (any, 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.TypeFor[*Config](), 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 "simd/archsimd", 178 "github.com/mmcloughlin/avo/build", 179 "github.com/mmcloughlin/avo/operand", 180 "github.com/mmcloughlin/avo/reg", 181 }, 182 HTTPStatusCodeWhitelist: []string{"200", "400", "404", "500"}, 183 } 184 185 const ConfigName = "staticcheck.conf" 186 187 type ParseError struct { 188 Filename string 189 toml.ParseError 190 } 191 192 func parseConfigs(dir string) ([]Config, error) { 193 var out []Config 194 195 // TODO(dh): consider stopping at the GOPATH/module boundary 196 for dir != "" { 197 path := filepath.Join(dir, ConfigName) 198 fi, err := os.Stat(path) 199 if os.IsNotExist(err) || (err == nil && !fi.Mode().IsRegular()) { 200 // walk up 201 ndir := filepath.Dir(dir) 202 if ndir == dir { 203 break 204 } 205 dir = ndir 206 continue 207 } 208 if err != nil { 209 return nil, err 210 } 211 212 // There is a small TOCTOU window here, but we're fine with reporting an 213 // error if the source tree is modified concurrently in weird ways while 214 // running Staticcheck. 215 f, err := os.Open(path) 216 if err != nil { 217 return nil, err 218 } 219 220 var cfg Config 221 _, err = toml.NewDecoder(f).Decode(&cfg) 222 f.Close() 223 if err != nil { 224 if err, ok := err.(toml.ParseError); ok { 225 return nil, ParseError{ 226 Filename: filepath.Join(dir, ConfigName), 227 ParseError: err, 228 } 229 } 230 return nil, err 231 } 232 out = append(out, cfg) 233 ndir := filepath.Dir(dir) 234 if ndir == dir { 235 break 236 } 237 dir = ndir 238 } 239 out = append(out, DefaultConfig) 240 if len(out) < 2 { 241 return out, nil 242 } 243 for i := 0; i < len(out)/2; i++ { 244 out[i], out[len(out)-1-i] = out[len(out)-1-i], out[i] 245 } 246 return out, nil 247 } 248 249 func mergeConfigs(confs []Config) Config { 250 if len(confs) == 0 { 251 // This shouldn't happen because we always have at least a 252 // default config. 253 panic("trying to merge zero configs") 254 } 255 if len(confs) == 1 { 256 return confs[0] 257 } 258 conf := confs[0] 259 for _, oconf := range confs[1:] { 260 conf = conf.Merge(oconf) 261 } 262 return conf 263 } 264 265 func Load(dir string) (Config, error) { 266 confs, err := parseConfigs(dir) 267 if err != nil { 268 return Config{}, err 269 } 270 conf := mergeConfigs(confs) 271 272 conf.Checks = normalizeList(conf.Checks) 273 conf.Initialisms = normalizeList(conf.Initialisms) 274 conf.DotImportWhitelist = normalizeList(conf.DotImportWhitelist) 275 conf.HTTPStatusCodeWhitelist = normalizeList(conf.HTTPStatusCodeWhitelist) 276 277 return conf, nil 278 }