modify goanalysis

This commit is contained in:
Denis Isaev 2019-03-17 22:46:18 +03:00 committed by Isaev Denis
parent 926e99aed0
commit 7289a90245
2 changed files with 46 additions and 523 deletions

View File

@ -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)
}

View File

@ -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...)