go/analysis/unitchecker: a main function for vet-lite tools

(By "vet lite", we mean static tools that must be invoked by a build
system, such as 'go vet'.)

This CL publishes the former internal/unitchecker package.
Its misnamed Main function is renamed Run, and it has a new Main
that does the steps of a real main (log, flag, etc).

The motivation for this change is to reduce cmd/vet-lite to the point
of triviality to simplify the maintenance of the vendored copy of
x/tools in GOROOT, because GOROOT/src/cmd/vet will need a copy of that
logic. It is now essentially a one-liner.

Also, improve usage messages; analysisflags.PrintUsage wasn't
appropriate for all callers so it has been eliminated.
Each of {single,multi,unit}checker prints its own 1-line usage message.

Change-Id: I214c0e4ae7a2923eee8df3f7548341f2320cad2b
Reviewed-on: https://go-review.googlesource.com/c/149742
Reviewed-by: Michael Matloob <matloob@golang.org>
This commit is contained in:
Alan Donovan 2018-11-15 10:33:45 -05:00
parent 1fdeb1692e
commit 17409aa234
9 changed files with 131 additions and 143 deletions

View File

@ -8,14 +8,8 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/internal/analysisflags"
"golang.org/x/tools/go/analysis/internal/unitchecker"
"golang.org/x/tools/go/analysis/unitchecker"
"golang.org/x/tools/go/analysis/passes/asmdecl"
"golang.org/x/tools/go/analysis/passes/assign"
@ -41,7 +35,34 @@ import (
"golang.org/x/tools/go/analysis/passes/unusedresult"
)
var analyzers = []*analysis.Analyzer{
// 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
// other tristate flags, requiring the -all flag to reenable them.
// (By itself, -all did not enable all checkers.)
// The -all flag is no longer needed, so it is a no-op.
//
// The shadow analyzer has been removed from the suite,
// but can be run using these additional commands:
// $ go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow
// $ go vet -vettool=$(which shadow)
// 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(
asmdecl.Analyzer,
assign.Analyzer,
atomic.Analyzer,
@ -64,62 +85,5 @@ var analyzers = []*analysis.Analyzer{
unreachable.Analyzer,
unsafeptr.Analyzer,
unusedresult.Analyzer,
}
func main() {
log.SetFlags(0)
log.SetPrefix("vet: ")
if err := analysis.Validate(analyzers); err != nil {
log.Fatal(err)
}
// 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
// other tristate flags, requiring the -all flag to reenable them.
// (By itself, -all did not enable all checkers.)
// The -all flag is no longer needed, so it is a no-op.
//
// The shadow analyzer has been removed from the suite,
// but can be run using these additional commands:
// $ go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow
// $ go vet -vettool=$(which shadow)
// Alternatively, one could build a multichecker containing all
// the desired checks (vet's suite + shadow) and run it in a
// single "go vet" command.
for _, name := range []string{"source", "v", "all"} {
_ = flag.Bool(name, false, "no effect (deprecated)")
}
_ = flag.String("tags", "", "no effect (deprecated)")
flag.Usage = func() {
fmt.Fprintln(os.Stderr, `Usage of vet:
vet unit.cfg # execute analysis specified by config file
vet help # general help
vet help name # help on specific analyzer and its flags`)
flag.PrintDefaults()
os.Exit(1)
}
analyzers = analysisflags.Parse(analyzers, true)
args := flag.Args()
if len(args) == 0 {
flag.Usage()
}
if args[0] == "help" {
analysisflags.Help("vet", analyzers, args[1:])
os.Exit(0)
}
if len(args) != 1 || !strings.HasSuffix(args[0], ".cfg") {
log.Fatalf("invalid command: want .cfg file (this reduced version of vet is intended to be run only by the 'go vet' command)")
}
unitchecker.Main(args[0], analyzers)
)
}

View File

@ -3,39 +3,28 @@ package analysisflags
import (
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sort"
"strings"
"golang.org/x/tools/go/analysis"
)
const usage = `PROGNAME is a tool for static analysis of Go programs.
const help = `PROGNAME is a tool for static analysis of Go programs.
PROGNAME examines Go source code and reports suspicious constructs, such as Printf
calls whose arguments do not align with the format string. It uses heuristics
that do not guarantee all reports are genuine problems, but it can find errors
not caught by the compilers.
Usage: PROGNAME [-flag] [package]
PROGNAME examines Go source code and reports suspicious constructs,
such as Printf calls whose arguments do not align with the format
string. It uses heuristics that do not guarantee all reports are
genuine problems, but it can find errors not caught by the compilers.
`
// PrintUsage prints the usage message to stderr.
func PrintUsage(out io.Writer) {
progname := filepath.Base(os.Args[0])
fmt.Fprintln(out, strings.Replace(usage, "PROGNAME", progname, -1))
}
// Help implements the help subcommand for a multichecker or vet-lite
// style command. The optional args specify the analyzers to describe.
// Help calls log.Fatal if no such analyzer exists.
func Help(progname string, analyzers []*analysis.Analyzer, args []string) {
// No args: show summary of all analyzers.
if len(args) == 0 {
PrintUsage(os.Stdout)
fmt.Println(strings.Replace(help, "PROGNAME", progname, -1))
fmt.Println("Registered analyzers:")
fmt.Println()
sort.Slice(analyzers, func(i, j int) bool {

View File

@ -25,7 +25,6 @@ import (
"time"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/internal/unitchecker"
"golang.org/x/tools/go/packages"
)
@ -108,18 +107,6 @@ func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) {
}()
}
// The undocumented protocol used by 'go vet'
// is that a vet-like tool must support:
//
// -flags describe flags in JSON
// -V=full describe executable for build caching
// foo.cfg perform separate modular analyze on the single
// unit described by a JSON config file foo.cfg.
if len(args) == 1 && strings.HasSuffix(args[0], ".cfg") {
unitchecker.Main(args[0], analyzers)
panic("unreachable")
}
// Load the packages.
if dbg('v') {
log.SetPrefix("")

View File

@ -13,10 +13,12 @@ import (
"log"
"os"
"path/filepath"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/internal/analysisflags"
"golang.org/x/tools/go/analysis/internal/checker"
"golang.org/x/tools/go/analysis/unitchecker"
)
func Main(analyzers ...*analysis.Analyzer) {
@ -34,10 +36,13 @@ func Main(analyzers ...*analysis.Analyzer) {
args := flag.Args()
if len(args) == 0 {
analysisflags.PrintUsage(os.Stderr)
fmt.Fprintf(os.Stderr, "Run '%[1]s help' for more detail,\n"+
" or '%[1]s help name' for details and flags of a specific analyzer.\n",
progname)
fmt.Fprintf(os.Stderr, `%[1]s is a tool for static analysis of Go programs.
Usage: %[1]s [-flag] [package]
Run '%[1]s help' for more detail,
or '%[1]s help name' for details and flags of a specific analyzer.
`, progname)
os.Exit(1)
}
@ -46,5 +51,10 @@ func Main(analyzers ...*analysis.Analyzer) {
os.Exit(0)
}
if len(args) == 1 && strings.HasSuffix(args[0], ".cfg") {
unitchecker.Run(args[0], analyzers)
panic("unreachable")
}
os.Exit(checker.Run(args, analyzers))
}

View File

@ -33,6 +33,7 @@ import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/internal/analysisflags"
"golang.org/x/tools/go/analysis/internal/checker"
"golang.org/x/tools/go/analysis/unitchecker"
)
// Main is the main function for a checker command for a single analysis.
@ -67,5 +68,10 @@ func Main(a *analysis.Analyzer) {
os.Exit(1)
}
if len(args) == 1 && strings.HasSuffix(args[0], ".cfg") {
unitchecker.Run(args[0], analyzers)
panic("unreachable")
}
os.Exit(checker.Run(args, analyzers))
}

View File

@ -30,6 +30,7 @@ package unitchecker
import (
"encoding/gob"
"encoding/json"
"flag"
"fmt"
"go/ast"
"go/build"
@ -41,12 +42,14 @@ import (
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/internal/analysisflags"
"golang.org/x/tools/go/analysis/internal/facts"
)
@ -68,9 +71,56 @@ type Config struct {
SucceedOnTypecheckFailure bool
}
// Main reads the *.cfg file, runs the analysis,
// Main is the main function of a vet-like analysis tool that must be
// invoked by a build system to analyze a single package.
//
// The protocol required by 'go vet -vettool=...' is that the tool must support:
//
// -flags describe flags in JSON
// -V=full describe executable for build caching
// foo.cfg perform separate modular analyze on the single
// unit described by a JSON config file foo.cfg.
//
func Main(analyzers ...*analysis.Analyzer) {
progname := filepath.Base(os.Args[0])
log.SetFlags(0)
log.SetPrefix(progname + ": ")
if err := analysis.Validate(analyzers); err != nil {
log.Fatal(err)
}
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `%[1]s is a tool for static analysis of Go programs.
Usage of %[1]s:
%.16[1]s unit.cfg # execute analysis specified by config file
%.16[1]s help # general help
%.16[1]s help name # help on specific analyzer and its flags
`, progname)
os.Exit(1)
}
analyzers = analysisflags.Parse(analyzers, true)
args := flag.Args()
if len(args) == 0 {
flag.Usage()
}
if args[0] == "help" {
analysisflags.Help(progname, analyzers, args[1:])
os.Exit(0)
}
if len(args) != 1 || !strings.HasSuffix(args[0], ".cfg") {
log.Fatalf("invalid command: want .cfg file (this reduced version of %s is intended to be run only by the 'go vet' command)", progname)
}
Run(args[0], analyzers)
}
// Run reads the *.cfg file, runs the analysis,
// and calls os.Exit with an appropriate error code.
func Main(configFile string, analyzers []*analysis.Analyzer) {
// It assumes flags have already been set.
func Run(configFile string, analyzers []*analysis.Analyzer) {
cfg, err := readConfig(configFile)
if err != nil {
log.Fatal(err)

View File

@ -12,19 +12,15 @@ package unitchecker_test
import (
"flag"
"log"
"os"
"os/exec"
"runtime"
"strings"
"testing"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/analysistest"
"golang.org/x/tools/go/analysis/internal/analysisflags"
"golang.org/x/tools/go/analysis/internal/unitchecker"
"golang.org/x/tools/go/analysis/passes/findcall"
"golang.org/x/tools/go/analysis/passes/printf"
"golang.org/x/tools/go/analysis/unitchecker"
)
func TestMain(m *testing.M) {
@ -40,24 +36,10 @@ func TestMain(m *testing.M) {
}
func main() {
findcall.Analyzer.Flags.Set("name", "MyFunc123")
var analyzers = []*analysis.Analyzer{
unitchecker.Main(
findcall.Analyzer,
printf.Analyzer,
}
if err := analysis.Validate(analyzers); err != nil {
log.Fatal(err)
}
analyzers = analysisflags.Parse(analyzers, true)
args := flag.Args()
if len(args) != 1 || !strings.HasSuffix(args[0], ".cfg") {
log.Fatalf("invalid command: want .cfg file")
}
unitchecker.Main(args[0], analyzers)
)
}
// This is a very basic integration test of modular
@ -72,7 +54,7 @@ func TestIntegration(t *testing.T) {
testdata := analysistest.TestData()
cmd := exec.Command("go", "vet", "-vettool="+os.Args[0], "b")
cmd := exec.Command("go", "vet", "-vettool="+os.Args[0], "-findcall.name=MyFunc123", "b")
cmd.Env = append(os.Environ(),
"UNITCHECKER_CHILD=1",
"GOPATH="+testdata,