diff --git a/pkg/golinters/goanalysis/analysisflags/flags.go b/pkg/golinters/goanalysis/analysisflags/flags.go deleted file mode 100644 index a03a185f..00000000 --- a/pkg/golinters/goanalysis/analysisflags/flags.go +++ /dev/null @@ -1,344 +0,0 @@ -// Copyright 2018 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. - -// Package analysisflags defines helpers for processing flags of -// analysis driver tools. -package analysisflags - -import ( - "crypto/sha256" - "encoding/json" - "flag" - "fmt" - "go/token" - "io" - "io/ioutil" - "log" - "os" - "strconv" - "strings" - - "golang.org/x/tools/go/analysis" -) - -// flags common to all {single,multi,unit}checkers. -var ( - JSON = false // -json - Context = -1 // -c=N: if N>0, display offending line plus N lines of context -) - -// Parse creates a flag for each of the analyzer's flags, -// including (in multi mode) a flag named after the analyzer, -// parses the flags, then filters and returns the list of -// analyzers enabled by flags. -func Parse(analyzers []*analysis.Analyzer, multi bool) []*analysis.Analyzer { - // Connect each analysis flag to the command line as -analysis.flag. - enabled := make(map[*analysis.Analyzer]*triState) - for _, a := range analyzers { - var prefix string - - // Add -NAME flag to enable it. - if multi { - prefix = a.Name + "." - - enable := new(triState) - enableUsage := "enable " + a.Name + " analysis" - flag.Var(enable, a.Name, enableUsage) - enabled[a] = enable - } - - a.Flags.VisitAll(func(f *flag.Flag) { - if !multi && flag.Lookup(f.Name) != nil { - log.Printf("%s flag -%s would conflict with driver; skipping", a.Name, f.Name) - return - } - - name := prefix + f.Name - flag.Var(f.Value, name, f.Usage) - }) - } - - // standard flags: -flags, -V. - printflags := flag.Bool("flags", false, "print analyzer flags in JSON") - addVersionFlag() - - // flags common to all checkers - flag.BoolVar(&JSON, "json", JSON, "emit JSON output") - flag.IntVar(&Context, "c", Context, `display offending line with this many lines of context`) - - // Add shims for legacy vet flags to enable existing - // scripts that run vet to continue to work. - _ = flag.Bool("source", false, "no effect (deprecated)") - _ = flag.Bool("v", false, "no effect (deprecated)") - _ = flag.Bool("all", false, "no effect (deprecated)") - _ = flag.String("tags", "", "no effect (deprecated)") - for old, new := range vetLegacyFlags { - newFlag := flag.Lookup(new) - if newFlag != nil && flag.Lookup(old) == nil { - flag.Var(newFlag.Value, old, "deprecated alias for -"+new) - } - } - - flag.Parse() // (ExitOnError) - - // -flags: print flags so that go vet knows which ones are legitimate. - if *printflags { - printFlags() - os.Exit(0) - } - - // If any -NAME flag is true, run only those analyzers. Otherwise, - // if any -NAME flag is false, run all but those analyzers. - if multi { - var hasTrue, hasFalse bool - for _, ts := range enabled { - switch *ts { - case setTrue: - hasTrue = true - case setFalse: - hasFalse = true - } - } - - var keep []*analysis.Analyzer - if hasTrue { - for _, a := range analyzers { - if *enabled[a] == setTrue { - keep = append(keep, a) - } - } - analyzers = keep - } else if hasFalse { - for _, a := range analyzers { - if *enabled[a] != setFalse { - keep = append(keep, a) - } - } - analyzers = keep - } - } - - return analyzers -} - -func printFlags() { - type jsonFlag struct { - Name string - Bool bool - Usage string - } - var flags []jsonFlag = nil - flag.VisitAll(func(f *flag.Flag) { - // Don't report {single,multi}checker debugging - // flags as these have no effect on unitchecker - // (as invoked by 'go vet'). - switch f.Name { - case "debug", "cpuprofile", "memprofile", "trace": - return - } - - b, ok := f.Value.(interface{ IsBoolFlag() bool }) - isBool := ok && b.IsBoolFlag() - flags = append(flags, jsonFlag{f.Name, isBool, f.Usage}) - }) - data, err := json.MarshalIndent(flags, "", "\t") - if err != nil { - log.Fatal(err) - } - os.Stdout.Write(data) -} - -// addVersionFlag registers a -V flag that, if set, -// prints the executable version and exits 0. -// -// If the -V flag already exists — for example, because it was already -// registered by a call to cmd/internal/objabi.AddVersionFlag — then -// addVersionFlag does nothing. -func addVersionFlag() { - if flag.Lookup("V") == nil { - flag.Var(versionFlag{}, "V", "print version and exit") - } -} - -// versionFlag minimally complies with the -V protocol required by "go vet". -type versionFlag struct{} - -func (versionFlag) IsBoolFlag() bool { return true } -func (versionFlag) Get() interface{} { return nil } -func (versionFlag) String() string { return "" } -func (versionFlag) Set(s string) error { - if s != "full" { - log.Fatalf("unsupported flag value: -V=%s", s) - } - - // This replicates the miminal subset of - // cmd/internal/objabi.AddVersionFlag, which is private to the - // go tool yet forms part of our command-line interface. - // TODO(adonovan): clarify the contract. - - // Print the tool version so the build system can track changes. - // Formats: - // $progname version devel ... buildID=... - // $progname version go1.9.1 - progname := os.Args[0] - f, err := os.Open(progname) - if err != nil { - log.Fatal(err) - } - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - log.Fatal(err) - } - f.Close() - fmt.Printf("%s version devel comments-go-here buildID=%02x\n", - progname, string(h.Sum(nil))) - os.Exit(0) - return nil -} - -// A triState is a boolean that knows whether -// it has been set to either true or false. -// It is used to identify whether a flag appears; -// the standard boolean flag cannot -// distinguish missing from unset. -// It also satisfies flag.Value. -type triState int - -const ( - unset triState = iota - setTrue - setFalse -) - -func triStateFlag(name string, value triState, usage string) *triState { - flag.Var(&value, name, usage) - return &value -} - -// triState implements flag.Value, flag.Getter, and flag.boolFlag. -// They work like boolean flags: we can say vet -printf as well as vet -printf=true -func (ts *triState) Get() interface{} { - return *ts == setTrue -} - -func (ts triState) isTrue() bool { - return ts == setTrue -} - -func (ts *triState) Set(value string) error { - b, err := strconv.ParseBool(value) - if err != nil { - // This error message looks poor but package "flag" adds - // "invalid boolean value %q for -NAME: %s" - return fmt.Errorf("want true or false") - } - if b { - *ts = setTrue - } else { - *ts = setFalse - } - return nil -} - -func (ts *triState) String() string { - switch *ts { - case unset: - return "true" - case setTrue: - return "true" - case setFalse: - return "false" - } - panic("not reached") -} - -func (ts triState) IsBoolFlag() bool { - return true -} - -// Legacy flag support - -// vetLegacyFlags maps flags used by legacy vet to their corresponding -// new names. The old names will continue to work. -var vetLegacyFlags = map[string]string{ - // Analyzer name changes - "bool": "bools", - "buildtags": "buildtag", - "methods": "stdmethods", - "rangeloops": "loopclosure", - - // Analyzer flags - "compositewhitelist": "composites.whitelist", - "printfuncs": "printf.funcs", - "shadowstrict": "shadow.strict", - "unusedfuncs": "unusedresult.funcs", - "unusedstringmethods": "unusedresult.stringmethods", -} - -// ---- output helpers common to all drivers ---- - -// PrintPlain prints a diagnostic in plain text form, -// with context specified by the -c flag. -func PrintPlain(fset *token.FileSet, diag analysis.Diagnostic) { - posn := fset.Position(diag.Pos) - fmt.Fprintf(os.Stderr, "%s: %s\n", posn, diag.Message) - - // -c=N: show offending line plus N lines of context. - if Context >= 0 { - data, _ := ioutil.ReadFile(posn.Filename) - lines := strings.Split(string(data), "\n") - for i := posn.Line - Context; i <= posn.Line+Context; i++ { - if 1 <= i && i <= len(lines) { - fmt.Fprintf(os.Stderr, "%d\t%s\n", i, lines[i-1]) - } - } - } -} - -// A JSONTree is a mapping from package ID to analysis name to result. -// Each result is either a jsonError or a list of jsonDiagnostic. -type JSONTree map[string]map[string]interface{} - -// Add adds the result of analysis 'name' on package 'id'. -// The result is either a list of diagnostics or an error. -func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.Diagnostic, err error) { - var v interface{} - if err != nil { - type jsonError struct { - Err string `json:"error"` - } - v = jsonError{err.Error()} - } else if len(diags) > 0 { - type jsonDiagnostic struct { - Category string `json:"category,omitempty"` - Posn string `json:"posn"` - Message string `json:"message"` - } - var diagnostics []jsonDiagnostic - for _, f := range diags { - diagnostics = append(diagnostics, jsonDiagnostic{ - Category: f.Category, - Posn: fset.Position(f.Pos).String(), - Message: f.Message, - }) - } - v = diagnostics - } - if v != nil { - m, ok := tree[id] - if !ok { - m = make(map[string]interface{}) - tree[id] = m - } - m[name] = v - } -} - -func (tree JSONTree) Print() { - data, err := json.MarshalIndent(tree, "", "\t") - if err != nil { - log.Panicf("internal error: JSON marshalling failed: %v", err) - } - fmt.Printf("%s\n", data) -} diff --git a/pkg/golinters/goanalysis/checker/checker.go b/pkg/golinters/goanalysis/checker/checker.go index ec80606a..56942b80 100644 --- a/pkg/golinters/goanalysis/checker/checker.go +++ b/pkg/golinters/goanalysis/checker/checker.go @@ -12,7 +12,6 @@ package checker import ( "bytes" "encoding/gob" - "flag" "fmt" "go/token" "go/types" @@ -27,10 +26,10 @@ import ( "sync" "time" + "github.com/pkg/errors" + "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" - - "github.com/golangci/golangci-lint/pkg/golinters/goanalysis/analysisflags" ) var ( @@ -42,22 +41,16 @@ var ( // t show [t]iming info (NB: use 'p' flag to avoid GC/scheduler noise) // v show [v]erbose logging // - Debug = "" + Debug = os.Getenv("GL_DEBUG_GO_ANALYSIS") // Log files for optional performance tracing. CPUProfile, MemProfile, Trace string ) -// RegisterFlags registers command-line flags used by the analysis driver. -func RegisterFlags() { - // When adding flags here, remember to update - // the list of suppressed flags in analysisflags. - - flag.StringVar(&Debug, "debug", Debug, `debug flags, any subset of "fpstv"`) - - flag.StringVar(&CPUProfile, "cpuprofile", "", "write CPU profile to this file") - flag.StringVar(&MemProfile, "memprofile", "", "write memory profile to this file") - flag.StringVar(&Trace, "trace", "", "write trace log to this file") +type Diagnostic struct { + analysis.Diagnostic + AnalyzerName string + Position token.Position } // Run loads the packages specified by args using go/packages, @@ -66,7 +59,8 @@ func RegisterFlags() { // It provides most of the logic for the main functions of both the // singlechecker and the multi-analysis commands. // It returns the appropriate exit code. -func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) { +//nolint:gocyclo +func Run(analyzers []*analysis.Analyzer, initialPackages []*packages.Package) ([]Diagnostic, []error) { if CPUProfile != "" { f, err := os.Create(CPUProfile) if err != nil { @@ -113,81 +107,13 @@ func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) { if dbg('v') { log.SetPrefix("") log.SetFlags(log.Lmicroseconds) // display timing - log.Printf("load %s", args) - } - - // Optimization: if the selected analyzers don't produce/consume - // facts, we need source only for the initial packages. - allSyntax := needFacts(analyzers) - initial, err := load(args, allSyntax) - if err != nil { - log.Print(err) - return 1 // load errors + log.Printf("load %d packages", len(initialPackages)) } // Print the results. - roots := analyze(initial, analyzers) + roots := analyze(initialPackages, analyzers) - return printDiagnostics(roots) -} - -// load loads the initial packages. -func load(patterns []string, allSyntax bool) ([]*packages.Package, error) { - mode := packages.LoadSyntax - if allSyntax { - mode = packages.LoadAllSyntax - } - conf := packages.Config{ - Mode: mode, - Tests: true, - } - initial, err := packages.Load(&conf, patterns...) - if err == nil { - if n := packages.PrintErrors(initial); n > 1 { - err = fmt.Errorf("%d errors during loading", n) - } else if n == 1 { - err = fmt.Errorf("error during loading") - } else if len(initial) == 0 { - err = fmt.Errorf("%s matched no packages", strings.Join(patterns, " ")) - } - } - - return initial, err -} - -// TestAnalyzer applies an analysis to a set of packages (and their -// dependencies if necessary) and returns the results. -// -// Facts about pkg are returned in a map keyed by object; package facts -// have a nil key. -// -// This entry point is used only by analysistest. -func TestAnalyzer(a *analysis.Analyzer, pkgs []*packages.Package) []*TestAnalyzerResult { - var results []*TestAnalyzerResult - for _, act := range analyze(pkgs, []*analysis.Analyzer{a}) { - facts := make(map[types.Object][]analysis.Fact) - for key, fact := range act.objectFacts { - if key.obj.Pkg() == act.pass.Pkg { - facts[key.obj] = append(facts[key.obj], fact) - } - } - for key, fact := range act.packageFacts { - if key.pkg == act.pass.Pkg { - facts[nil] = append(facts[nil], fact) - } - } - - results = append(results, &TestAnalyzerResult{act.pass, act.diagnostics, facts, act.result, act.err}) - } - return results -} - -type TestAnalyzerResult struct { - Pass *analysis.Pass - Diagnostics []analysis.Diagnostic - Facts map[types.Object][]analysis.Fact - Result interface{} - Err error + return extractDiagnostics(roots) } func analyze(pkgs []*packages.Package, analyzers []*analysis.Analyzer) []*action { @@ -252,118 +178,59 @@ func analyze(pkgs []*packages.Package, analyzers []*analysis.Analyzer) []*action return roots } -// printDiagnostics prints the diagnostics for the root packages in either -// plain text or JSON format. JSON format also includes errors for any -// dependencies. -// -// It returns the exitcode: in plain mode, 0 for success, 1 for analysis -// errors, and 3 for diagnostics. We avoid 2 since the flag package uses -// it. JSON mode always succeeds at printing errors and diagnostics in a -// structured form to stdout. -func printDiagnostics(roots []*action) (exitcode int) { - // Print the output. - // - // Print diagnostics only for root packages, - // but errors for all packages. - printed := make(map[*action]bool) - var print func(*action) +func extractDiagnostics(roots []*action) (retDiags []Diagnostic, retErrors []error) { + extracted := make(map[*action]bool) + var extract func(*action) var visitAll func(actions []*action) visitAll = func(actions []*action) { for _, act := range actions { - if !printed[act] { - printed[act] = true + if !extracted[act] { + extracted[act] = true visitAll(act.deps) - print(act) + extract(act) } } } - if analysisflags.JSON { - // JSON output - tree := make(analysisflags.JSONTree) - print = func(act *action) { - var diags []analysis.Diagnostic - if act.isroot { - diags = act.diagnostics - } - tree.Add(act.pkg.Fset, act.pkg.ID, act.a.Name, diags, act.err) + // De-duplicate diagnostics by position (not token.Pos) to + // avoid double-reporting in source files that belong to + // multiple packages, such as foo and foo.test. + type key struct { + token.Position + *analysis.Analyzer + message string + } + seen := make(map[key]bool) + + extract = func(act *action) { + if act.err != nil { + retErrors = append(retErrors, errors.Wrap(act.err, act.a.Name)) + return } - visitAll(roots) - tree.Print() - } else { - // plain text output - // De-duplicate diagnostics by position (not token.Pos) to - // avoid double-reporting in source files that belong to - // multiple packages, such as foo and foo.test. - type key struct { - token.Position - *analysis.Analyzer - message string - } - seen := make(map[key]bool) + if act.isroot { + for _, diag := range act.diagnostics { + // We don't display a.Name/f.Category + // as most users don't care. - print = func(act *action) { - if act.err != nil { - fmt.Fprintf(os.Stderr, "%s: %v\n", act.a.Name, act.err) - exitcode = 1 // analysis failed, at least partially - return - } - if act.isroot { - for _, diag := range act.diagnostics { - // We don't display a.Name/f.Category - // as most users don't care. - - posn := act.pkg.Fset.Position(diag.Pos) - k := key{posn, act.a, diag.Message} - if seen[k] { - continue // duplicate - } - seen[k] = true - - analysisflags.PrintPlain(act.pkg.Fset, diag) + posn := act.pkg.Fset.Position(diag.Pos) + k := key{posn, act.a, diag.Message} + if seen[k] { + continue // duplicate } - } - } - visitAll(roots) + seen[k] = true - if exitcode == 0 && len(seen) > 0 { - exitcode = 3 // successfuly produced diagnostics - } - } - - // Print timing info. - if dbg('t') { - if !dbg('p') { - log.Println("Warning: times are mostly GC/scheduler noise; use -debug=tp to disable parallelism") - } - var all []*action - var total time.Duration - for act := range printed { - all = append(all, act) - total += act.duration - } - sort.Slice(all, func(i, j int) bool { - return all[i].duration > all[j].duration - }) - - // Print actions accounting for 90% of the total. - var sum time.Duration - for _, act := range all { - fmt.Fprintf(os.Stderr, "%s\t%s\n", act.duration, act) - sum += act.duration - if sum >= total*9/10 { - break + retDiags = append(retDiags, Diagnostic{Diagnostic: diag, AnalyzerName: act.a.Name, Position: posn}) } } } - - return exitcode + visitAll(roots) + return } -// needFacts reports whether any analysis required by the specified set +// NeedFacts reports whether any analysis required by the specified set // needs facts. If so, we must load the entire program from source. -func needFacts(analyzers []*analysis.Analyzer) bool { +func NeedFacts(analyzers []*analysis.Analyzer) bool { seen := make(map[*analysis.Analyzer]bool) var q []*analysis.Analyzer // for BFS q = append(q, analyzers...)