 6a979fb40d
			
		
	
	
		6a979fb40d
		
			
		
	
	
	
	
		
			
			* update staticcheck Don't fork staticcheck: use the upstream version. Remove unneeded SSA loading. * Cache go/analysis facts Don't load unneeded packages for go/analysis. Repeated run of go/analysis linters now 10x faster (2s vs 20s on this repo) than before.
		
			
				
	
	
		
			393 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			393 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) 2013 The Go Authors. All rights reserved.
 | |
| //
 | |
| // Use of this source code is governed by a BSD-style
 | |
| // license that can be found in the LICENSE file or at
 | |
| // https://developers.google.com/open-source/licenses/bsd.
 | |
| 
 | |
| // Package lintutil provides helpers for writing linter command lines.
 | |
| package lintutil // import "honnef.co/go/tools/lint/lintutil"
 | |
| 
 | |
| import (
 | |
| 	"crypto/sha256"
 | |
| 	"errors"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	"go/build"
 | |
| 	"go/token"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"os"
 | |
| 	"os/signal"
 | |
| 	"regexp"
 | |
| 	"runtime"
 | |
| 	"runtime/pprof"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync/atomic"
 | |
| 
 | |
| 	"honnef.co/go/tools/config"
 | |
| 	"honnef.co/go/tools/internal/cache"
 | |
| 	"honnef.co/go/tools/lint"
 | |
| 	"honnef.co/go/tools/lint/lintutil/format"
 | |
| 	"honnef.co/go/tools/version"
 | |
| 
 | |
| 	"golang.org/x/tools/go/analysis"
 | |
| 	"golang.org/x/tools/go/buildutil"
 | |
| 	"golang.org/x/tools/go/packages"
 | |
| )
 | |
| 
 | |
| func NewVersionFlag() flag.Getter {
 | |
| 	tags := build.Default.ReleaseTags
 | |
| 	v := tags[len(tags)-1][2:]
 | |
| 	version := new(VersionFlag)
 | |
| 	if err := version.Set(v); err != nil {
 | |
| 		panic(fmt.Sprintf("internal error: %s", err))
 | |
| 	}
 | |
| 	return version
 | |
| }
 | |
| 
 | |
| type VersionFlag int
 | |
| 
 | |
| func (v *VersionFlag) String() string {
 | |
| 	return fmt.Sprintf("1.%d", *v)
 | |
| 
 | |
| }
 | |
| 
 | |
| func (v *VersionFlag) Set(s string) error {
 | |
| 	if len(s) < 3 {
 | |
| 		return errors.New("invalid Go version")
 | |
| 	}
 | |
| 	if s[0] != '1' {
 | |
| 		return errors.New("invalid Go version")
 | |
| 	}
 | |
| 	if s[1] != '.' {
 | |
| 		return errors.New("invalid Go version")
 | |
| 	}
 | |
| 	i, err := strconv.Atoi(s[2:])
 | |
| 	*v = VersionFlag(i)
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (v *VersionFlag) Get() interface{} {
 | |
| 	return int(*v)
 | |
| }
 | |
| 
 | |
| func usage(name string, flags *flag.FlagSet) func() {
 | |
| 	return func() {
 | |
| 		fmt.Fprintf(os.Stderr, "Usage of %s:\n", name)
 | |
| 		fmt.Fprintf(os.Stderr, "\t%s [flags] # runs on package in current directory\n", name)
 | |
| 		fmt.Fprintf(os.Stderr, "\t%s [flags] packages\n", name)
 | |
| 		fmt.Fprintf(os.Stderr, "\t%s [flags] directory\n", name)
 | |
| 		fmt.Fprintf(os.Stderr, "\t%s [flags] files... # must be a single package\n", name)
 | |
| 		fmt.Fprintf(os.Stderr, "Flags:\n")
 | |
| 		flags.PrintDefaults()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type list []string
 | |
| 
 | |
| func (list *list) String() string {
 | |
| 	return `"` + strings.Join(*list, ",") + `"`
 | |
| }
 | |
| 
 | |
| func (list *list) Set(s string) error {
 | |
| 	if s == "" {
 | |
| 		*list = nil
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	*list = strings.Split(s, ",")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func FlagSet(name string) *flag.FlagSet {
 | |
| 	flags := flag.NewFlagSet("", flag.ExitOnError)
 | |
| 	flags.Usage = usage(name, flags)
 | |
| 	flags.String("tags", "", "List of `build tags`")
 | |
| 	flags.Bool("tests", true, "Include tests")
 | |
| 	flags.Bool("version", false, "Print version and exit")
 | |
| 	flags.Bool("show-ignored", false, "Don't filter ignored problems")
 | |
| 	flags.String("f", "text", "Output `format` (valid choices are 'stylish', 'text' and 'json')")
 | |
| 	flags.String("explain", "", "Print description of `check`")
 | |
| 
 | |
| 	flags.String("debug.cpuprofile", "", "Write CPU profile to `file`")
 | |
| 	flags.String("debug.memprofile", "", "Write memory profile to `file`")
 | |
| 	flags.Bool("debug.version", false, "Print detailed version information about this program")
 | |
| 	flags.Bool("debug.no-compile-errors", false, "Don't print compile errors")
 | |
| 
 | |
| 	checks := list{"inherit"}
 | |
| 	fail := list{"all"}
 | |
| 	flags.Var(&checks, "checks", "Comma-separated list of `checks` to enable.")
 | |
| 	flags.Var(&fail, "fail", "Comma-separated list of `checks` that can cause a non-zero exit status.")
 | |
| 
 | |
| 	tags := build.Default.ReleaseTags
 | |
| 	v := tags[len(tags)-1][2:]
 | |
| 	version := new(VersionFlag)
 | |
| 	if err := version.Set(v); err != nil {
 | |
| 		panic(fmt.Sprintf("internal error: %s", err))
 | |
| 	}
 | |
| 
 | |
| 	flags.Var(version, "go", "Target Go `version` in the format '1.x'")
 | |
| 	return flags
 | |
| }
 | |
| 
 | |
| func findCheck(cs []*analysis.Analyzer, check string) (*analysis.Analyzer, bool) {
 | |
| 	for _, c := range cs {
 | |
| 		if c.Name == check {
 | |
| 			return c, true
 | |
| 		}
 | |
| 	}
 | |
| 	return nil, false
 | |
| }
 | |
| 
 | |
| func ProcessFlagSet(cs []*analysis.Analyzer, cums []lint.CumulativeChecker, fs *flag.FlagSet) {
 | |
| 	tags := fs.Lookup("tags").Value.(flag.Getter).Get().(string)
 | |
| 	tests := fs.Lookup("tests").Value.(flag.Getter).Get().(bool)
 | |
| 	goVersion := fs.Lookup("go").Value.(flag.Getter).Get().(int)
 | |
| 	formatter := fs.Lookup("f").Value.(flag.Getter).Get().(string)
 | |
| 	printVersion := fs.Lookup("version").Value.(flag.Getter).Get().(bool)
 | |
| 	showIgnored := fs.Lookup("show-ignored").Value.(flag.Getter).Get().(bool)
 | |
| 	explain := fs.Lookup("explain").Value.(flag.Getter).Get().(string)
 | |
| 
 | |
| 	cpuProfile := fs.Lookup("debug.cpuprofile").Value.(flag.Getter).Get().(string)
 | |
| 	memProfile := fs.Lookup("debug.memprofile").Value.(flag.Getter).Get().(string)
 | |
| 	debugVersion := fs.Lookup("debug.version").Value.(flag.Getter).Get().(bool)
 | |
| 	debugNoCompile := fs.Lookup("debug.no-compile-errors").Value.(flag.Getter).Get().(bool)
 | |
| 
 | |
| 	cfg := config.Config{}
 | |
| 	cfg.Checks = *fs.Lookup("checks").Value.(*list)
 | |
| 
 | |
| 	exit := func(code int) {
 | |
| 		if cpuProfile != "" {
 | |
| 			pprof.StopCPUProfile()
 | |
| 		}
 | |
| 		if memProfile != "" {
 | |
| 			f, err := os.Create(memProfile)
 | |
| 			if err != nil {
 | |
| 				panic(err)
 | |
| 			}
 | |
| 			runtime.GC()
 | |
| 			pprof.WriteHeapProfile(f)
 | |
| 		}
 | |
| 		os.Exit(code)
 | |
| 	}
 | |
| 	if cpuProfile != "" {
 | |
| 		f, err := os.Create(cpuProfile)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 		pprof.StartCPUProfile(f)
 | |
| 	}
 | |
| 
 | |
| 	if debugVersion {
 | |
| 		version.Verbose()
 | |
| 		exit(0)
 | |
| 	}
 | |
| 
 | |
| 	if printVersion {
 | |
| 		version.Print()
 | |
| 		exit(0)
 | |
| 	}
 | |
| 
 | |
| 	// Validate that the tags argument is well-formed. go/packages
 | |
| 	// doesn't detect malformed build flags and returns unhelpful
 | |
| 	// errors.
 | |
| 	tf := buildutil.TagsFlag{}
 | |
| 	if err := tf.Set(tags); err != nil {
 | |
| 		fmt.Fprintln(os.Stderr, fmt.Errorf("invalid value %q for flag -tags: %s", tags, err))
 | |
| 		exit(1)
 | |
| 	}
 | |
| 
 | |
| 	if explain != "" {
 | |
| 		var haystack []*analysis.Analyzer
 | |
| 		haystack = append(haystack, cs...)
 | |
| 		for _, cum := range cums {
 | |
| 			haystack = append(haystack, cum.Analyzer())
 | |
| 		}
 | |
| 		check, ok := findCheck(haystack, explain)
 | |
| 		if !ok {
 | |
| 			fmt.Fprintln(os.Stderr, "Couldn't find check", explain)
 | |
| 			exit(1)
 | |
| 		}
 | |
| 		if check.Doc == "" {
 | |
| 			fmt.Fprintln(os.Stderr, explain, "has no documentation")
 | |
| 			exit(1)
 | |
| 		}
 | |
| 		fmt.Println(check.Doc)
 | |
| 		exit(0)
 | |
| 	}
 | |
| 
 | |
| 	ps, err := Lint(cs, cums, fs.Args(), &Options{
 | |
| 		Tags:      tags,
 | |
| 		LintTests: tests,
 | |
| 		GoVersion: goVersion,
 | |
| 		Config:    cfg,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		fmt.Fprintln(os.Stderr, err)
 | |
| 		exit(1)
 | |
| 	}
 | |
| 
 | |
| 	var f format.Formatter
 | |
| 	switch formatter {
 | |
| 	case "text":
 | |
| 		f = format.Text{W: os.Stdout}
 | |
| 	case "stylish":
 | |
| 		f = &format.Stylish{W: os.Stdout}
 | |
| 	case "json":
 | |
| 		f = format.JSON{W: os.Stdout}
 | |
| 	default:
 | |
| 		fmt.Fprintf(os.Stderr, "unsupported output format %q\n", formatter)
 | |
| 		exit(2)
 | |
| 	}
 | |
| 
 | |
| 	var (
 | |
| 		total    int
 | |
| 		errors   int
 | |
| 		warnings int
 | |
| 	)
 | |
| 
 | |
| 	fail := *fs.Lookup("fail").Value.(*list)
 | |
| 	analyzers := make([]*analysis.Analyzer, len(cs), len(cs)+len(cums))
 | |
| 	copy(analyzers, cs)
 | |
| 	for _, cum := range cums {
 | |
| 		analyzers = append(analyzers, cum.Analyzer())
 | |
| 	}
 | |
| 	shouldExit := lint.FilterChecks(analyzers, fail)
 | |
| 	shouldExit["compile"] = true
 | |
| 
 | |
| 	total = len(ps)
 | |
| 	for _, p := range ps {
 | |
| 		if p.Check == "compile" && debugNoCompile {
 | |
| 			continue
 | |
| 		}
 | |
| 		if p.Severity == lint.Ignored && !showIgnored {
 | |
| 			continue
 | |
| 		}
 | |
| 		if shouldExit[p.Check] {
 | |
| 			errors++
 | |
| 		} else {
 | |
| 			p.Severity = lint.Warning
 | |
| 			warnings++
 | |
| 		}
 | |
| 		f.Format(p)
 | |
| 	}
 | |
| 	if f, ok := f.(format.Statter); ok {
 | |
| 		f.Stats(total, errors, warnings)
 | |
| 	}
 | |
| 	if errors > 0 {
 | |
| 		exit(1)
 | |
| 	}
 | |
| 	exit(0)
 | |
| }
 | |
| 
 | |
| type Options struct {
 | |
| 	Config config.Config
 | |
| 
 | |
| 	Tags      string
 | |
| 	LintTests bool
 | |
| 	GoVersion int
 | |
| }
 | |
| 
 | |
| func computeSalt() ([]byte, error) {
 | |
| 	if version.Version != "devel" {
 | |
| 		return []byte(version.Version), nil
 | |
| 	}
 | |
| 	p, err := os.Executable()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	f, err := os.Open(p)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer f.Close()
 | |
| 	h := sha256.New()
 | |
| 	if _, err := io.Copy(h, f); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return h.Sum(nil), nil
 | |
| }
 | |
| 
 | |
| func Lint(cs []*analysis.Analyzer, cums []lint.CumulativeChecker, paths []string, opt *Options) ([]lint.Problem, error) {
 | |
| 	salt, err := computeSalt()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("could not compute salt for cache: %s", err)
 | |
| 	}
 | |
| 	cache.SetSalt(salt)
 | |
| 
 | |
| 	if opt == nil {
 | |
| 		opt = &Options{}
 | |
| 	}
 | |
| 
 | |
| 	l := &lint.Linter{
 | |
| 		Checkers:           cs,
 | |
| 		CumulativeCheckers: cums,
 | |
| 		GoVersion:          opt.GoVersion,
 | |
| 		Config:             opt.Config,
 | |
| 	}
 | |
| 	cfg := &packages.Config{}
 | |
| 	if opt.LintTests {
 | |
| 		cfg.Tests = true
 | |
| 	}
 | |
| 	if opt.Tags != "" {
 | |
| 		cfg.BuildFlags = append(cfg.BuildFlags, "-tags", opt.Tags)
 | |
| 	}
 | |
| 
 | |
| 	printStats := func() {
 | |
| 		// Individual stats are read atomically, but overall there
 | |
| 		// is no synchronisation. For printing rough progress
 | |
| 		// information, this doesn't matter.
 | |
| 		switch atomic.LoadUint32(&l.Stats.State) {
 | |
| 		case lint.StateInitializing:
 | |
| 			fmt.Fprintln(os.Stderr, "Status: initializing")
 | |
| 		case lint.StateGraph:
 | |
| 			fmt.Fprintln(os.Stderr, "Status: loading package graph")
 | |
| 		case lint.StateProcessing:
 | |
| 			fmt.Fprintf(os.Stderr, "Packages: %d/%d initial, %d/%d total; Workers: %d/%d; Problems: %d\n",
 | |
| 				atomic.LoadUint32(&l.Stats.ProcessedInitialPackages),
 | |
| 				atomic.LoadUint32(&l.Stats.InitialPackages),
 | |
| 				atomic.LoadUint32(&l.Stats.ProcessedPackages),
 | |
| 				atomic.LoadUint32(&l.Stats.TotalPackages),
 | |
| 				atomic.LoadUint32(&l.Stats.ActiveWorkers),
 | |
| 				atomic.LoadUint32(&l.Stats.TotalWorkers),
 | |
| 				atomic.LoadUint32(&l.Stats.Problems),
 | |
| 			)
 | |
| 		case lint.StateCumulative:
 | |
| 			fmt.Fprintln(os.Stderr, "Status: processing cumulative checkers")
 | |
| 		}
 | |
| 	}
 | |
| 	if len(infoSignals) > 0 {
 | |
| 		ch := make(chan os.Signal, 1)
 | |
| 		signal.Notify(ch, infoSignals...)
 | |
| 		defer signal.Stop(ch)
 | |
| 		go func() {
 | |
| 			for range ch {
 | |
| 				printStats()
 | |
| 			}
 | |
| 		}()
 | |
| 	}
 | |
| 
 | |
| 	return l.Lint(cfg, paths)
 | |
| }
 | |
| 
 | |
| var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?$`)
 | |
| 
 | |
| func parsePos(pos string) token.Position {
 | |
| 	if pos == "-" || pos == "" {
 | |
| 		return token.Position{}
 | |
| 	}
 | |
| 	parts := posRe.FindStringSubmatch(pos)
 | |
| 	if parts == nil {
 | |
| 		panic(fmt.Sprintf("internal error: malformed position %q", pos))
 | |
| 	}
 | |
| 	file := parts[1]
 | |
| 	line, _ := strconv.Atoi(parts[2])
 | |
| 	col, _ := strconv.Atoi(parts[3])
 | |
| 	return token.Position{
 | |
| 		Filename: file,
 | |
| 		Line:     line,
 | |
| 		Column:   col,
 | |
| 	}
 | |
| }
 |