go/analysis: harmonize flags across all checkers

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>
diff --git a/go/analysis/cmd/vet-lite/main.go b/go/analysis/cmd/vet-lite/main.go
index cf7441a..259d397 100644
--- a/go/analysis/cmd/vet-lite/main.go
+++ b/go/analysis/cmd/vet-lite/main.go
@@ -7,8 +7,6 @@
 package main
 
 import (
-	"flag"
-
 	"golang.org/x/tools/go/analysis/unitchecker"
 
 	"golang.org/x/tools/go/analysis/passes/asmdecl"
@@ -34,11 +32,6 @@
 	"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
@@ -53,12 +46,6 @@
 // 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(
diff --git a/go/analysis/internal/analysisflags/flags.go b/go/analysis/internal/analysisflags/flags.go
index d915be6..729ac3b 100644
--- a/go/analysis/internal/analysisflags/flags.go
+++ b/go/analysis/internal/analysisflags/flags.go
@@ -11,27 +11,29 @@
 	"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
@@ -44,7 +46,6 @@
 			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) {
@@ -55,12 +56,6 @@
 
 			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})
 		})
 	}
 
@@ -68,7 +63,16 @@
 	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 {
@@ -80,11 +84,7 @@
 
 	// -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)
 	}
 
@@ -122,6 +122,33 @@
 	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.
 //
@@ -247,3 +274,70 @@
 	"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/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go
index 80fa02f..6c65179 100644
--- a/go/analysis/internal/checker/checker.go
+++ b/go/analysis/internal/checker/checker.go
@@ -7,12 +7,10 @@
 import (
 	"bytes"
 	"encoding/gob"
-	"encoding/json"
 	"flag"
 	"fmt"
 	"go/token"
 	"go/types"
-	"io/ioutil"
 	"log"
 	"os"
 	"reflect"
@@ -25,12 +23,11 @@
 	"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
@@ -41,17 +38,16 @@
 	//
 	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")
@@ -276,50 +272,18 @@
 		}
 	}
 
-	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.
+			var diags []analysis.Diagnostic
+			if act.isroot {
+				diags = act.diagnostics
 			}
-			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
-			}
+			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
 
@@ -340,30 +304,18 @@
 				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)
 				}
 			}
 		}
diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go
index edfca57..7b8fec9 100644
--- a/go/analysis/unitchecker/unitchecker.go
+++ b/go/analysis/unitchecker/unitchecker.go
@@ -25,7 +25,6 @@
 //   so we will not get to analyze it. Yet we must in order
 //   to create base facts for, say, the fmt package for the
 //   printf checker.
-// - support JSON output, factored with multichecker.
 
 import (
 	"encoding/gob"
@@ -57,6 +56,7 @@
 // It is provided to the tool in a JSON-encoded file
 // whose name ends with ".cfg".
 type Config struct {
+	ID                        string // e.g. "fmt [fmt.test]"
 	Compiler                  string
 	Dir                       string
 	ImportPath                string
@@ -127,16 +127,37 @@
 	}
 
 	fset := token.NewFileSet()
-	diags, err := run(fset, cfg, analyzers)
+	results, err := run(fset, cfg, analyzers)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	if !cfg.VetxOnly && len(diags) > 0 {
-		for _, diag := range diags {
-			fmt.Fprintf(os.Stderr, "%s: %s\n", fset.Position(diag.Pos), diag.Message)
+	// In VetxOnly mode, the analysis is run only for facts.
+	if !cfg.VetxOnly {
+		if analysisflags.JSON {
+			// JSON output
+			tree := make(analysisflags.JSONTree)
+			for _, res := range results {
+				tree.Add(fset, cfg.ID, res.a.Name, res.diagnostics, res.err)
+			}
+			tree.Print()
+		} else {
+			// plain text
+			exit := 0
+			for _, res := range results {
+				if res.err != nil {
+					log.Println(res.err)
+					exit = 1
+				}
+			}
+			for _, res := range results {
+				for _, diag := range res.diagnostics {
+					analysisflags.PrintPlain(fset, diag)
+					exit = 1
+				}
+			}
+			os.Exit(exit)
 		}
-		os.Exit(1)
 	}
 
 	os.Exit(0)
@@ -160,7 +181,7 @@
 	return cfg, nil
 }
 
-func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]analysis.Diagnostic, error) {
+func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]result, error) {
 	// Load, parse, typecheck.
 	var files []*ast.File
 	for _, name := range cfg.GoFiles {
@@ -333,14 +354,13 @@
 
 	execAll(analyzers)
 
-	// Return diagnostics from root analyzers.
-	var diags []analysis.Diagnostic
-	for _, a := range analyzers {
+	// Return diagnostics and errors from root analyzers.
+	results := make([]result, len(analyzers))
+	for i, a := range analyzers {
 		act := actions[a]
-		if act.err != nil {
-			return nil, act.err // some analysis failed
-		}
-		diags = append(diags, act.diagnostics...)
+		results[i].a = a
+		results[i].err = act.err
+		results[i].diagnostics = act.diagnostics
 	}
 
 	data := facts.Encode()
@@ -348,7 +368,13 @@
 		return nil, fmt.Errorf("failed to write analysis facts: %v", err)
 	}
 
-	return diags, nil
+	return results, nil
+}
+
+type result struct {
+	a           *analysis.Analyzer
+	diagnostics []analysis.Diagnostic
+	err         error
 }
 
 type importerFunc func(path string) (*types.Package, error)
diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go
index c6bb2bf..3fc12a5 100644
--- a/go/analysis/unitchecker/unitchecker_test.go
+++ b/go/analysis/unitchecker/unitchecker_test.go
@@ -60,14 +60,29 @@
 testdata/src/b/b.go:6:13: call of MyFunc123(...)
 testdata/src/b/b.go:7:11: call of MyFunc123(...)
 `
+	const wantAJSON = `# a
+{
+	"a": {
+		"findcall": [
+			{
+				"posn": "$GOPATH/src/a/a.go:4:11",
+				"message": "call of MyFunc123(...)"
+			}
+		]
+	}
+}
+`
 
 	for _, test := range []struct {
-		args string
-		want string
+		args     string
+		wantOut  string
+		wantExit int
 	}{
-		{args: "a", want: wantA},
-		{args: "b", want: wantB},
-		{args: "a b", want: wantA + wantB},
+		{args: "a", wantOut: wantA, wantExit: 2},
+		{args: "b", wantOut: wantB, wantExit: 2},
+		{args: "a b", wantOut: wantA + wantB, wantExit: 2},
+		{args: "-json a", wantOut: wantAJSON, wantExit: 0},
+		{args: "-c=0 a", wantOut: wantA + "4		MyFunc123()\n", wantExit: 2},
 	} {
 		cmd := exec.Command("go", "vet", "-vettool="+os.Args[0], "-findcall.name=MyFunc123")
 		cmd.Args = append(cmd.Args, strings.Fields(test.args)...)
@@ -77,16 +92,17 @@
 		)
 
 		out, err := cmd.CombinedOutput()
-		exitcode := -1
+		exitcode := 0
 		if exitErr, ok := err.(*exec.ExitError); ok {
 			exitcode = exitErr.ExitCode()
 		}
-		if exitcode != 2 {
-			t.Errorf("%s: got exit code %d, want 2", test.args, exitcode)
+		if exitcode != test.wantExit {
+			t.Errorf("%s: got exit code %d, want %d", test.args, exitcode, test.wantExit)
 		}
+		got := strings.Replace(string(out), testdata, "$GOPATH", -1)
 
-		if got := string(out); got != test.want {
-			t.Errorf("%s: got <<%s>>, want <<%s>>", test.args, got, test.want)
+		if got != test.wantOut {
+			t.Errorf("%s: got <<%s>>, want <<%s>>", test.args, got, test.wantOut)
 		}
 	}
 }