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"},
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