--- /dev/null
+// 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"
+ "sync/atomic"
+ "time"
+
+ "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")
+ flags.String("debug.measure-analyzers", "", "Write analysis measurements to `file`. `file` will be opened for appending if it already exists.")
+ flags.Uint("debug.repeat-analyzers", 0, "Run analyzers `num` times")
+
+ 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)
+ debugRepeat := fs.Lookup("debug.repeat-analyzers").Value.(flag.Getter).Get().(uint)
+
+ var measureAnalyzers func(analysis *analysis.Analyzer, pkg *lint.Package, d time.Duration)
+ if path := fs.Lookup("debug.measure-analyzers").Value.(flag.Getter).Get().(string); path != "" {
+ f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ mu := &sync.Mutex{}
+ measureAnalyzers = func(analysis *analysis.Analyzer, pkg *lint.Package, d time.Duration) {
+ mu.Lock()
+ defer mu.Unlock()
+ if _, err := fmt.Fprintf(f, "%s\t%s\t%d\n", analysis.Name, pkg.ID, d.Nanoseconds()); err != nil {
+ log.Println("error writing analysis measurements:", err)
+ }
+ }
+ }
+
+ 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,
+ PrintAnalyzerMeasurement: measureAnalyzers,
+ RepeatAnalyzers: debugRepeat,
+ })
+ 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
+ ignored 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 {
+ ignored++
+ 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, ignored)
+ }
+ if errors > 0 {
+ exit(1)
+ }
+ exit(0)
+}
+
+type Options struct {
+ Config config.Config
+
+ Tags string
+ LintTests bool
+ GoVersion int
+ PrintAnalyzerMeasurement func(analysis *analysis.Analyzer, pkg *lint.Package, d time.Duration)
+ RepeatAnalyzers uint
+}
+
+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,
+ RepeatAnalyzers: opt.RepeatAnalyzers,
+ }
+ l.Stats.PrintAnalyzerMeasurement = opt.PrintAnalyzerMeasurement
+ 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()
+ }
+ }()
+ }
+
+ ps, err := l.Lint(cfg, paths)
+ return ps, err
+}
+
+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,
+ }
+}
+
+func InitializeAnalyzers(docs map[string]*lint.Documentation, analyzers map[string]*analysis.Analyzer) map[string]*analysis.Analyzer {
+ out := make(map[string]*analysis.Analyzer, len(analyzers))
+ for k, v := range analyzers {
+ vc := *v
+ out[k] = &vc
+
+ vc.Name = k
+ doc, ok := docs[k]
+ if !ok {
+ panic(fmt.Sprintf("missing documentation for check %s", k))
+ }
+ vc.Doc = doc.String()
+ if vc.Flags.Usage == nil {
+ fs := flag.NewFlagSet("", flag.PanicOnError)
+ fs.Var(NewVersionFlag(), "go", "Target Go version")
+ vc.Flags = *fs
+ }
+ }
+ return out
+}