github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/config/config.go (about)

     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  }