2019-11-07 20:05:39 +01:00
|
|
|
package config
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
2020-02-26 19:46:17 +01:00
|
|
|
"go/ast"
|
|
|
|
"go/token"
|
2019-11-07 20:05:39 +01:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"reflect"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/BurntSushi/toml"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
|
|
)
|
|
|
|
|
2020-02-26 19:46:17 +01:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2019-11-07 20:05:39 +01:00
|
|
|
var Analyzer = &analysis.Analyzer{
|
|
|
|
Name: "config",
|
|
|
|
Doc: "loads configuration for the current package tree",
|
|
|
|
Run: func(pass *analysis.Pass) (interface{}, error) {
|
2020-02-26 19:46:17 +01:00
|
|
|
dir := dirAST(pass.Files, pass.Fset)
|
|
|
|
if dir == "" {
|
2019-11-07 20:05:39 +01:00
|
|
|
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{
|
2020-02-26 19:46:17 +01:00
|
|
|
Checks: []string{"all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"},
|
2019-11-07 20:05:39 +01:00
|
|
|
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",
|
2020-02-26 19:46:17 +01:00
|
|
|
"XSS", "SIP", "RTP", "AMQP", "DB", "TS",
|
2019-11-07 20:05:39 +01:00
|
|
|
},
|
|
|
|
DotImportWhitelist: []string{},
|
|
|
|
HTTPStatusCodeWhitelist: []string{"200", "400", "404", "500"},
|
|
|
|
}
|
|
|
|
|
2020-02-26 19:46:17 +01:00
|
|
|
const ConfigName = "staticcheck.conf"
|
2019-11-07 20:05:39 +01:00
|
|
|
|
|
|
|
func parseConfigs(dir string) ([]Config, error) {
|
|
|
|
var out []Config
|
|
|
|
|
|
|
|
// TODO(dh): consider stopping at the GOPATH/module boundary
|
|
|
|
for dir != "" {
|
2020-02-26 19:46:17 +01:00
|
|
|
f, err := os.Open(filepath.Join(dir, ConfigName))
|
2019-11-07 20:05:39 +01:00
|
|
|
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
|
|
|
|
}
|