1package config 2 3import ( 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. 22func 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 51func 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 59var 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 78func For(pass *analysis.Pass) *Config { 79 return pass.ResultOf[Analyzer].(*Config) 80} 81 82func 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 95func 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 118func (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 134type 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 148func (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 159var DefaultConfig = Config{ 160 Checks: []string{"all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-ST1023"}, 161 Initialisms: []string{ 162 "ACL", "API", "ASCII", "CPU", "CSS", "DNS", 163 "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", 164 "IP", "JSON", "QPS", "RAM", "RPC", "SLA", 165 "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", 166 "UDP", "UI", "GID", "UID", "UUID", "URI", 167 "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", 168 "XSS", "SIP", "RTP", "AMQP", "DB", "TS", 169 }, 170 DotImportWhitelist: []string{}, 171 HTTPStatusCodeWhitelist: []string{"200", "400", "404", "500"}, 172} 173 174const ConfigName = "staticcheck.conf" 175 176func parseConfigs(dir string) ([]Config, error) { 177 var out []Config 178 179 // TODO(dh): consider stopping at the GOPATH/module boundary 180 for dir != "" { 181 f, err := os.Open(filepath.Join(dir, ConfigName)) 182 if os.IsNotExist(err) { 183 ndir := filepath.Dir(dir) 184 if ndir == dir { 185 break 186 } 187 dir = ndir 188 continue 189 } 190 if err != nil { 191 return nil, err 192 } 193 var cfg Config 194 _, err = toml.DecodeReader(f, &cfg) 195 f.Close() 196 if err != nil { 197 return nil, err 198 } 199 out = append(out, cfg) 200 ndir := filepath.Dir(dir) 201 if ndir == dir { 202 break 203 } 204 dir = ndir 205 } 206 out = append(out, DefaultConfig) 207 if len(out) < 2 { 208 return out, nil 209 } 210 for i := 0; i < len(out)/2; i++ { 211 out[i], out[len(out)-1-i] = out[len(out)-1-i], out[i] 212 } 213 return out, nil 214} 215 216func mergeConfigs(confs []Config) Config { 217 if len(confs) == 0 { 218 // This shouldn't happen because we always have at least a 219 // default config. 220 panic("trying to merge zero configs") 221 } 222 if len(confs) == 1 { 223 return confs[0] 224 } 225 conf := confs[0] 226 for _, oconf := range confs[1:] { 227 conf = conf.Merge(oconf) 228 } 229 return conf 230} 231 232func Load(dir string) (Config, error) { 233 confs, err := parseConfigs(dir) 234 if err != nil { 235 return Config{}, err 236 } 237 conf := mergeConfigs(confs) 238 239 conf.Checks = normalizeList(conf.Checks) 240 conf.Initialisms = normalizeList(conf.Initialisms) 241 conf.DotImportWhitelist = normalizeList(conf.DotImportWhitelist) 242 conf.HTTPStatusCodeWhitelist = normalizeList(conf.HTTPStatusCodeWhitelist) 243 244 return conf, nil 245} 246