Skip to content

Commit

Permalink
go/analysis: harmonize flags across all checkers
Browse files Browse the repository at this point in the history
The -json and -c=N flags, formerly belonging only to the
go/packages-based {single,multi}checkers, are now supported by
unitchecker as well.

The no-op -source, -v, -all, and -tags flags, formerly belonging only
to unitchecker, have moved to the analysisflags package, which is
common to all checkers.

The -flags flag now reports all registered flags (except the
{single,multi}checker-only debugging flags) rather than just those
related to analyzers, allowing one to say: 'go vet -json' or 'go vet -c=1'.

The code for printing diagnostics, either plain or in JSON, has been
factored and moved into the common analysisflags package.

This CL depends on https://go-review.googlesource.com/c/go/+/149960 to
cmd/go, which causes 'go vet' to populate the ID field of the *.cfg.
This field is used as a key in the JSON tree.

Added basic tests of the new -json and -c unitchecker flags.

Change-Id: Ia7a3a9adc86de067de060732d2c200c58be3945a
Reviewed-on: https://go-review.googlesource.com/c/150038
Reviewed-by: Michael Matloob <matloob@golang.org>
  • Loading branch information
adonovan committed Nov 16, 2018
1 parent e3f267a commit 8e5aba0
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 122 deletions.
13 changes: 0 additions & 13 deletions go/analysis/cmd/vet-lite/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
package main

import (
"flag"

"golang.org/x/tools/go/analysis/unitchecker"

"golang.org/x/tools/go/analysis/passes/asmdecl"
Expand All @@ -34,11 +32,6 @@ import (
"golang.org/x/tools/go/analysis/passes/unusedresult"
)

// Flags for legacy vet compatibility.
//
// These flags, plus the shims in analysisflags, enable
// existing scripts that run vet to continue to work.
//
// Legacy vet had the concept of "experimental" checkers. There
// was exactly one, shadow, and it had to be explicitly enabled
// by the -shadow flag, which would of course disable all the
Expand All @@ -53,12 +46,6 @@ import (
// Alternatively, one could build a multichecker containing all
// the desired checks (vet's suite + shadow) and run it in a
// single "go vet" command.
func init() {
_ = 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)")
}

func main() {
unitchecker.Main(
Expand Down
134 changes: 114 additions & 20 deletions go/analysis/internal/analysisflags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,29 @@ import (
"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.
type analysisFlag struct {
Name string
Bool bool
Usage string
}
var analysisFlags []analysisFlag

enabled := make(map[*analysis.Analyzer]*triState)
for _, a := range analyzers {
var prefix string
Expand All @@ -44,7 +46,6 @@ func Parse(analyzers []*analysis.Analyzer, multi bool) []*analysis.Analyzer {
enableUsage := "enable " + a.Name + " analysis"
flag.Var(enable, a.Name, enableUsage)
enabled[a] = enable
analysisFlags = append(analysisFlags, analysisFlag{a.Name, true, enableUsage})
}

a.Flags.VisitAll(func(f *flag.Flag) {
Expand All @@ -55,20 +56,23 @@ func Parse(analyzers []*analysis.Analyzer, multi bool) []*analysis.Analyzer {

name := prefix + f.Name
flag.Var(f.Value, name, f.Usage)

var isBool bool
if b, ok := f.Value.(interface{ IsBoolFlag() bool }); ok {
isBool = b.IsBoolFlag()
}
analysisFlags = append(analysisFlags, analysisFlag{name, isBool, f.Usage})
})
}

// standard flags: -flags, -V.
printflags := flag.Bool("flags", false, "print analyzer flags in JSON")
addVersionFlag()

// Add shims for legacy vet flags.
// 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 {
Expand All @@ -80,11 +84,7 @@ func Parse(analyzers []*analysis.Analyzer, multi bool) []*analysis.Analyzer {

// -flags: print flags so that go vet knows which ones are legitimate.
if *printflags {
data, err := json.MarshalIndent(analysisFlags, "", "\t")
if err != nil {
log.Fatal(err)
}
os.Stdout.Write(data)
printFlags()
os.Exit(0)
}

Expand Down Expand Up @@ -122,6 +122,33 @@ func Parse(analyzers []*analysis.Analyzer, multi bool) []*analysis.Analyzer {
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.
//
Expand Down Expand Up @@ -247,3 +274,70 @@ var vetLegacyFlags = map[string]string{
"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)
}
80 changes: 16 additions & 64 deletions go/analysis/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ package checker
import (
"bytes"
"encoding/gob"
"encoding/json"
"flag"
"fmt"
"go/token"
"go/types"
"io/ioutil"
"log"
"os"
"reflect"
Expand All @@ -25,12 +23,11 @@ import (
"time"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/internal/analysisflags"
"golang.org/x/tools/go/packages"
)

var (
JSON = false

// Debug is a set of single-letter flags:
//
// f show [f]acts as they are created
Expand All @@ -41,17 +38,16 @@ var (
//
Debug = ""

Context = -1 // if >=0, display offending line plus this many lines of context

// Log files for optional performance tracing.
CPUProfile, MemProfile, Trace string
)

// RegisterFlags registers command-line flags used the analysis driver.
func RegisterFlags() {
flag.BoolVar(&JSON, "json", JSON, "emit JSON output")
// When adding flags here, remember to update
// the list of suppressed flags in analysisflags.

flag.StringVar(&Debug, "debug", Debug, `debug flags, any subset of "lpsv"`)
flag.IntVar(&Context, "c", Context, `display offending line with this many lines of context`)

flag.StringVar(&CPUProfile, "cpuprofile", "", "write CPU profile to this file")
flag.StringVar(&MemProfile, "memprofile", "", "write memory profile to this file")
Expand Down Expand Up @@ -276,50 +272,18 @@ func printDiagnostics(roots []*action) (exitcode int) {
}
}

if JSON {
tree := make(map[string]map[string]interface{}) // ID -> analysis -> result

if analysisflags.JSON {
// JSON output
tree := make(analysisflags.JSONTree)
print = func(act *action) {
m, existing := tree[act.pkg.ID]
if !existing {
m = make(map[string]interface{})
// Insert m into tree later iff non-empty.
}
if act.err != nil {
type jsonError struct {
Err string `json:"error"`
}
m[act.a.Name] = jsonError{act.err.Error()}
} else if act.isroot {
type jsonDiagnostic struct {
Category string `json:"category,omitempty"`
Posn string `json:"posn"`
Message string `json:"message"`
}
var diagnostics []jsonDiagnostic
for _, f := range act.diagnostics {
diagnostics = append(diagnostics, jsonDiagnostic{
Category: f.Category,
Posn: act.pkg.Fset.Position(f.Pos).String(),
Message: f.Message,
})
}
if diagnostics != nil {
m[act.a.Name] = diagnostics
}
}
if !existing && len(m) > 0 {
tree[act.pkg.ID] = m
var diags []analysis.Diagnostic
if act.isroot {
diags = act.diagnostics
}
tree.Add(act.pkg.Fset, act.pkg.ID, act.a.Name, diags, act.err)
}
visitAll(roots)

data, err := json.MarshalIndent(tree, "", "\t")
if err != nil {
log.Panicf("internal error: JSON marshalling failed: %v", err)
}
os.Stdout.Write(data)
fmt.Println()
tree.Print()
} else {
// plain text output

Expand All @@ -340,30 +304,18 @@ func printDiagnostics(roots []*action) (exitcode int) {
return
}
if act.isroot {
for _, f := range act.diagnostics {
for _, diag := range act.diagnostics {
// We don't display a.Name/f.Category
// as most users don't care.

posn := act.pkg.Fset.Position(f.Pos)

k := key{posn, act.a, f.Message}
posn := act.pkg.Fset.Position(diag.Pos)
k := key{posn, act.a, diag.Message}
if seen[k] {
continue // duplicate
}
seen[k] = true

fmt.Fprintf(os.Stderr, "%s: %s\n", posn, f.Message)

// -c=0: show offending line of code in 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])
}
}
}
analysisflags.PrintPlain(act.pkg.Fset, diag)
}
}
}
Expand Down
Loading

0 comments on commit 8e5aba0

Please sign in to comment.