package config import ( "bytes" "fmt" "go/ast" "go/token" "os" "path/filepath" "reflect" "strings" "github.com/BurntSushi/toml" "golang.org/x/tools/go/analysis" ) // Dir looks at a list of absolute file names, which should make up a // single package, and returns the path of the directory that may // contain a staticcheck.conf file. It returns the empty string if no // such directory could be determined, for example because all files // were located in Go's build cache. func Dir(files []string) string { if len(files) == 0 { return "" } cache, err := os.UserCacheDir() if err != nil { cache = "" } var path string for _, p := range files { // FIXME(dh): using strings.HasPrefix isn't technically // correct, but it should be good enough for now. if cache != "" && strings.HasPrefix(p, cache) { // File in the build cache of the standard Go build system continue } path = p break } if path == "" { // The package only consists of generated files. return "" } dir := filepath.Dir(path) return dir } func dirAST(files []*ast.File, fset *token.FileSet) string { names := make([]string, len(files)) for i, f := range files { names[i] = fset.PositionFor(f.Pos(), true).Filename } return Dir(names) } var Analyzer = &analysis.Analyzer{ Name: "config", Doc: "loads configuration for the current package tree", Run: func(pass *analysis.Pass) (interface{}, error) { dir := dirAST(pass.Files, pass.Fset) if dir == "" { cfg := DefaultConfig return &cfg, nil } cfg, err := Load(dir) if err != nil { return nil, fmt.Errorf("error loading staticcheck.conf: %s", err) } return &cfg, nil }, RunDespiteErrors: true, ResultType: reflect.TypeOf((*Config)(nil)), } func For(pass *analysis.Pass) *Config { return pass.ResultOf[Analyzer].(*Config) } func mergeLists(a, b []string) []string { out := make([]string, 0, len(a)+len(b)) for _, el := range b { if el == "inherit" { out = append(out, a...) } else { out = append(out, el) } } return out } func normalizeList(list []string) []string { if len(list) > 1 { nlist := make([]string, 0, len(list)) nlist = append(nlist, list[0]) for i, el := range list[1:] { if el != list[i] { nlist = append(nlist, el) } } list = nlist } for _, el := range list { if el == "inherit" { // This should never happen, because the default config // should not use "inherit" panic(`unresolved "inherit"`) } } return list } func (cfg Config) Merge(ocfg Config) Config { if ocfg.Checks != nil { cfg.Checks = mergeLists(cfg.Checks, ocfg.Checks) } if ocfg.Initialisms != nil { cfg.Initialisms = mergeLists(cfg.Initialisms, ocfg.Initialisms) } if ocfg.DotImportWhitelist != nil { cfg.DotImportWhitelist = mergeLists(cfg.DotImportWhitelist, ocfg.DotImportWhitelist) } if ocfg.HTTPStatusCodeWhitelist != nil { cfg.HTTPStatusCodeWhitelist = mergeLists(cfg.HTTPStatusCodeWhitelist, ocfg.HTTPStatusCodeWhitelist) } return cfg } type Config struct { // TODO(dh): this implementation makes it impossible for external // clients to add their own checkers with configuration. At the // moment, we don't really care about that; we don't encourage // that people use this package. In the future, we may. The // obvious solution would be using map[string]interface{}, but // that's obviously subpar. Checks []string `toml:"checks"` Initialisms []string `toml:"initialisms"` DotImportWhitelist []string `toml:"dot_import_whitelist"` HTTPStatusCodeWhitelist []string `toml:"http_status_code_whitelist"` } func (c Config) String() string { buf := &bytes.Buffer{} fmt.Fprintf(buf, "Checks: %#v\n", c.Checks) fmt.Fprintf(buf, "Initialisms: %#v\n", c.Initialisms) fmt.Fprintf(buf, "DotImportWhitelist: %#v\n", c.DotImportWhitelist) fmt.Fprintf(buf, "HTTPStatusCodeWhitelist: %#v", c.HTTPStatusCodeWhitelist) return buf.String() } var DefaultConfig = Config{ Checks: []string{"all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"}, Initialisms: []string{ "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS", }, DotImportWhitelist: []string{}, HTTPStatusCodeWhitelist: []string{"200", "400", "404", "500"}, } const ConfigName = "staticcheck.conf" func parseConfigs(dir string) ([]Config, error) { var out []Config // TODO(dh): consider stopping at the GOPATH/module boundary for dir != "" { f, err := os.Open(filepath.Join(dir, ConfigName)) if os.IsNotExist(err) { ndir := filepath.Dir(dir) if ndir == dir { break } dir = ndir continue } if err != nil { return nil, err } var cfg Config _, err = toml.DecodeReader(f, &cfg) f.Close() if err != nil { return nil, err } out = append(out, cfg) ndir := filepath.Dir(dir) if ndir == dir { break } dir = ndir } out = append(out, DefaultConfig) if len(out) < 2 { return out, nil } for i := 0; i < len(out)/2; i++ { out[i], out[len(out)-1-i] = out[len(out)-1-i], out[i] } return out, nil } func mergeConfigs(confs []Config) Config { if len(confs) == 0 { // This shouldn't happen because we always have at least a // default config. panic("trying to merge zero configs") } if len(confs) == 1 { return confs[0] } conf := confs[0] for _, oconf := range confs[1:] { conf = conf.Merge(oconf) } return conf } func Load(dir string) (Config, error) { confs, err := parseConfigs(dir) if err != nil { return Config{}, err } conf := mergeConfigs(confs) conf.Checks = normalizeList(conf.Checks) conf.Initialisms = normalizeList(conf.Initialisms) conf.DotImportWhitelist = normalizeList(conf.DotImportWhitelist) conf.HTTPStatusCodeWhitelist = normalizeList(conf.HTTPStatusCodeWhitelist) return conf, nil }