| // 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{} | |
| // A TextEdit describes the replacement of a portion of a file. | |
| // Start and End are zero-based half-open indices into the original byte | |
| // sequence of the file, and New is the new text. | |
| type JSONTextEdit struct { | |
| Filename string `json:"filename"` | |
| Start int `json:"start"` | |
| End int `json:"end"` | |
| New string `json:"new"` | |
| } | |
| // A JSONSuggestedFix describes an edit that should be applied as a whole or not | |
| // at all. It might contain multiple TextEdits/text_edits if the SuggestedFix | |
| // consists of multiple non-contiguous edits. | |
| type JSONSuggestedFix struct { | |
| Message string `json:"message"` | |
| Edits []JSONTextEdit `json:"edits"` | |
| } | |
| // A JSONDiagnostic can be used to encode and decode analysis.Diagnostics to and | |
| // from JSON. | |
| // TODO(matloob): Should the JSON diagnostics contain ranges? | |
| // If so, how should they be formatted? | |
| type JSONDiagnostic struct { | |
| Category string `json:"category,omitempty"` | |
| Posn string `json:"posn"` | |
| Message string `json:"message"` | |
| SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"` | |
| } | |
| // 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 { | |
| diagnostics := make([]JSONDiagnostic, 0, len(diags)) | |
| for _, f := range diags { | |
| var fixes []JSONSuggestedFix | |
| for _, fix := range f.SuggestedFixes { | |
| var edits []JSONTextEdit | |
| for _, edit := range fix.TextEdits { | |
| edits = append(edits, JSONTextEdit{ | |
| Filename: fset.Position(edit.Pos).Filename, | |
| Start: fset.Position(edit.Pos).Offset, | |
| End: fset.Position(edit.End).Offset, | |
| New: string(edit.NewText), | |
| }) | |
| } | |
| fixes = append(fixes, JSONSuggestedFix{ | |
| Message: fix.Message, | |
| Edits: edits, | |
| }) | |
| } | |
| jdiag := JSONDiagnostic{ | |
| Category: f.Category, | |
| Posn: fset.Position(f.Pos).String(), | |
| Message: f.Message, | |
| SuggestedFixes: fixes, | |
| } | |
| diagnostics = append(diagnostics, jdiag) | |
| } | |
| 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 marshaling failed: %v", err) | |
| } | |
| fmt.Printf("%s\n", data) | |
| } |