src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

config.go (7031B)


      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) (any, 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.TypeFor[*Config](),
     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 		"simd/archsimd",
    178 		"github.com/mmcloughlin/avo/build",
    179 		"github.com/mmcloughlin/avo/operand",
    180 		"github.com/mmcloughlin/avo/reg",
    181 	},
    182 	HTTPStatusCodeWhitelist: []string{"200", "400", "404", "500"},
    183 }
    184 
    185 const ConfigName = "staticcheck.conf"
    186 
    187 type ParseError struct {
    188 	Filename string
    189 	toml.ParseError
    190 }
    191 
    192 func parseConfigs(dir string) ([]Config, error) {
    193 	var out []Config
    194 
    195 	// TODO(dh): consider stopping at the GOPATH/module boundary
    196 	for dir != "" {
    197 		path := filepath.Join(dir, ConfigName)
    198 		fi, err := os.Stat(path)
    199 		if os.IsNotExist(err) || (err == nil && !fi.Mode().IsRegular()) {
    200 			// walk up
    201 			ndir := filepath.Dir(dir)
    202 			if ndir == dir {
    203 				break
    204 			}
    205 			dir = ndir
    206 			continue
    207 		}
    208 		if err != nil {
    209 			return nil, err
    210 		}
    211 
    212 		// There is a small TOCTOU window here, but we're fine with reporting an
    213 		// error if the source tree is modified concurrently in weird ways while
    214 		// running Staticcheck.
    215 		f, err := os.Open(path)
    216 		if err != nil {
    217 			return nil, err
    218 		}
    219 
    220 		var cfg Config
    221 		_, err = toml.NewDecoder(f).Decode(&cfg)
    222 		f.Close()
    223 		if err != nil {
    224 			if err, ok := err.(toml.ParseError); ok {
    225 				return nil, ParseError{
    226 					Filename:   filepath.Join(dir, ConfigName),
    227 					ParseError: err,
    228 				}
    229 			}
    230 			return nil, err
    231 		}
    232 		out = append(out, cfg)
    233 		ndir := filepath.Dir(dir)
    234 		if ndir == dir {
    235 			break
    236 		}
    237 		dir = ndir
    238 	}
    239 	out = append(out, DefaultConfig)
    240 	if len(out) < 2 {
    241 		return out, nil
    242 	}
    243 	for i := 0; i < len(out)/2; i++ {
    244 		out[i], out[len(out)-1-i] = out[len(out)-1-i], out[i]
    245 	}
    246 	return out, nil
    247 }
    248 
    249 func mergeConfigs(confs []Config) Config {
    250 	if len(confs) == 0 {
    251 		// This shouldn't happen because we always have at least a
    252 		// default config.
    253 		panic("trying to merge zero configs")
    254 	}
    255 	if len(confs) == 1 {
    256 		return confs[0]
    257 	}
    258 	conf := confs[0]
    259 	for _, oconf := range confs[1:] {
    260 		conf = conf.Merge(oconf)
    261 	}
    262 	return conf
    263 }
    264 
    265 func Load(dir string) (Config, error) {
    266 	confs, err := parseConfigs(dir)
    267 	if err != nil {
    268 		return Config{}, err
    269 	}
    270 	conf := mergeConfigs(confs)
    271 
    272 	conf.Checks = normalizeList(conf.Checks)
    273 	conf.Initialisms = normalizeList(conf.Initialisms)
    274 	conf.DotImportWhitelist = normalizeList(conf.DotImportWhitelist)
    275 	conf.HTTPStatusCodeWhitelist = normalizeList(conf.HTTPStatusCodeWhitelist)
    276 
    277 	return conf, nil
    278 }