config.go raw
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 }
267