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