// 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 }