389 lines
10 KiB
Go
389 lines
10 KiB
Go
// 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/gob"
|
|
"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.
|
|
//
|
|
// The result is intended to be passed to unitchecker.Run or checker.Run.
|
|
// Use in unitchecker.Run will gob.Register all fact types for the returned
|
|
// graph of analyzers but of course not the ones only reachable from
|
|
// dropped analyzers. To avoid inconsistency about which gob types are
|
|
// registered from run to run, Parse itself gob.Registers all the facts
|
|
// only reachable from dropped analyzers.
|
|
// This is not a particularly elegant API, but this is an internal package.
|
|
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)
|
|
}
|
|
|
|
everything := expand(analyzers)
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// Register fact types of skipped analyzers
|
|
// in case we encounter them in imported files.
|
|
kept := expand(analyzers)
|
|
for a := range everything {
|
|
if !kept[a] {
|
|
for _, f := range a.FactTypes {
|
|
gob.Register(f)
|
|
}
|
|
}
|
|
}
|
|
|
|
return analyzers
|
|
}
|
|
|
|
func expand(analyzers []*analysis.Analyzer) map[*analysis.Analyzer]bool {
|
|
seen := make(map[*analysis.Analyzer]bool)
|
|
var visitAll func([]*analysis.Analyzer)
|
|
visitAll = func(analyzers []*analysis.Analyzer) {
|
|
for _, a := range analyzers {
|
|
if !seen[a] {
|
|
seen[a] = true
|
|
visitAll(a.Requires)
|
|
}
|
|
}
|
|
}
|
|
visitAll(analyzers)
|
|
return seen
|
|
}
|
|
|
|
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 or fix as these have no effect on unitchecker
|
|
// (as invoked by 'go vet').
|
|
switch f.Name {
|
|
case "debug", "cpuprofile", "memprofile", "trace", "fix":
|
|
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 minimal 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 {
|
|
posn := fset.Position(diag.Pos)
|
|
end := fset.Position(diag.End)
|
|
if !end.IsValid() {
|
|
end = posn
|
|
}
|
|
data, _ := ioutil.ReadFile(posn.Filename)
|
|
lines := strings.Split(string(data), "\n")
|
|
for i := posn.Line - Context; i <= end.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
|
|
// TODO(matloob): Should the JSON diagnostics contain ranges?
|
|
// If so, how should they be formatted?
|
|
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)
|
|
}
|