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 ( import (
"flag" "flag"
"fmt"
"log"
"os"
"strings"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/unitchecker"
"golang.org/x/tools/go/analysis/internal/analysisflags"
"golang.org/x/tools/go/analysis/internal/unitchecker"
"golang.org/x/tools/go/analysis/passes/asmdecl" "golang.org/x/tools/go/analysis/passes/asmdecl"
"golang.org/x/tools/go/analysis/passes/assign" "golang.org/x/tools/go/analysis/passes/assign"
@ -41,85 +35,55 @@ import (
"golang.org/x/tools/go/analysis/passes/unusedresult" "golang.org/x/tools/go/analysis/passes/unusedresult"
) )
var analyzers = []*analysis.Analyzer{ // Flags for legacy vet compatibility.
asmdecl.Analyzer, //
assign.Analyzer, // These flags, plus the shims in analysisflags, enable
atomic.Analyzer, // existing scripts that run vet to continue to work.
bools.Analyzer, //
buildtag.Analyzer, // Legacy vet had the concept of "experimental" checkers. There
cgocall.Analyzer, // was exactly one, shadow, and it had to be explicitly enabled
composite.Analyzer, // by the -shadow flag, which would of course disable all the
copylock.Analyzer, // other tristate flags, requiring the -all flag to reenable them.
httpresponse.Analyzer, // (By itself, -all did not enable all checkers.)
loopclosure.Analyzer, // The -all flag is no longer needed, so it is a no-op.
lostcancel.Analyzer, //
nilfunc.Analyzer, // The shadow analyzer has been removed from the suite,
pkgfact.Analyzer, // but can be run using these additional commands:
printf.Analyzer, // $ go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow
shift.Analyzer, // $ go vet -vettool=$(which shadow)
stdmethods.Analyzer, // Alternatively, one could build a multichecker containing all
structtag.Analyzer, // the desired checks (vet's suite + shadow) and run it in a
tests.Analyzer, // single "go vet" command.
unmarshal.Analyzer, func init() {
unreachable.Analyzer, _ = flag.Bool("source", false, "no effect (deprecated)")
unsafeptr.Analyzer, _ = flag.Bool("v", false, "no effect (deprecated)")
unusedresult.Analyzer, _ = flag.Bool("all", false, "no effect (deprecated)")
_ = flag.String("tags", "", "no effect (deprecated)")
} }
func main() { func main() {
log.SetFlags(0) unitchecker.Main(
log.SetPrefix("vet: ") asmdecl.Analyzer,
assign.Analyzer,
if err := analysis.Validate(analyzers); err != nil { atomic.Analyzer,
log.Fatal(err) bools.Analyzer,
} buildtag.Analyzer,
cgocall.Analyzer,
// Flags for legacy vet compatibility. composite.Analyzer,
// copylock.Analyzer,
// These flags, plus the shims in analysisflags, enable httpresponse.Analyzer,
// existing scripts that run vet to continue to work. loopclosure.Analyzer,
// lostcancel.Analyzer,
// Legacy vet had the concept of "experimental" checkers. There nilfunc.Analyzer,
// was exactly one, shadow, and it had to be explicitly enabled pkgfact.Analyzer,
// by the -shadow flag, which would of course disable all the printf.Analyzer,
// other tristate flags, requiring the -all flag to reenable them. shift.Analyzer,
// (By itself, -all did not enable all checkers.) stdmethods.Analyzer,
// The -all flag is no longer needed, so it is a no-op. structtag.Analyzer,
// tests.Analyzer,
// The shadow analyzer has been removed from the suite, unmarshal.Analyzer,
// but can be run using these additional commands: unreachable.Analyzer,
// $ go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow unsafeptr.Analyzer,
// $ go vet -vettool=$(which shadow) unusedresult.Analyzer,
// 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 ( import (
"flag" "flag"
"fmt" "fmt"
"io"
"log" "log"
"os"
"path/filepath"
"sort" "sort"
"strings" "strings"
"golang.org/x/tools/go/analysis" "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 PROGNAME examines Go source code and reports suspicious constructs,
calls whose arguments do not align with the format string. It uses heuristics such as Printf calls whose arguments do not align with the format
that do not guarantee all reports are genuine problems, but it can find errors string. It uses heuristics that do not guarantee all reports are
not caught by the compilers. genuine problems, but it can find errors not caught by the compilers.
Usage: PROGNAME [-flag] [package]
` `
// 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 // Help implements the help subcommand for a multichecker or vet-lite
// style command. The optional args specify the analyzers to describe. // style command. The optional args specify the analyzers to describe.
// Help calls log.Fatal if no such analyzer exists. // Help calls log.Fatal if no such analyzer exists.
func Help(progname string, analyzers []*analysis.Analyzer, args []string) { func Help(progname string, analyzers []*analysis.Analyzer, args []string) {
// No args: show summary of all analyzers. // No args: show summary of all analyzers.
if len(args) == 0 { if len(args) == 0 {
PrintUsage(os.Stdout) fmt.Println(strings.Replace(help, "PROGNAME", progname, -1))
fmt.Println("Registered analyzers:") fmt.Println("Registered analyzers:")
fmt.Println() fmt.Println()
sort.Slice(analyzers, func(i, j int) bool { sort.Slice(analyzers, func(i, j int) bool {

View File

@ -25,7 +25,6 @@ import (
"time" "time"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/internal/unitchecker"
"golang.org/x/tools/go/packages" "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. // Load the packages.
if dbg('v') { if dbg('v') {
log.SetPrefix("") log.SetPrefix("")

View File

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

View File

@ -30,6 +30,7 @@ package unitchecker
import ( import (
"encoding/gob" "encoding/gob"
"encoding/json" "encoding/json"
"flag"
"fmt" "fmt"
"go/ast" "go/ast"
"go/build" "go/build"
@ -41,12 +42,14 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"path/filepath"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time" "time"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/internal/analysisflags"
"golang.org/x/tools/go/analysis/internal/facts" "golang.org/x/tools/go/analysis/internal/facts"
) )
@ -68,9 +71,56 @@ type Config struct {
SucceedOnTypecheckFailure bool 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. // 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) cfg, err := readConfig(configFile)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -12,19 +12,15 @@ package unitchecker_test
import ( import (
"flag" "flag"
"log"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"strings"
"testing" "testing"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/analysistest" "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/findcall"
"golang.org/x/tools/go/analysis/passes/printf" "golang.org/x/tools/go/analysis/passes/printf"
"golang.org/x/tools/go/analysis/unitchecker"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -40,24 +36,10 @@ func TestMain(m *testing.M) {
} }
func main() { func main() {
findcall.Analyzer.Flags.Set("name", "MyFunc123") unitchecker.Main(
var analyzers = []*analysis.Analyzer{
findcall.Analyzer, findcall.Analyzer,
printf.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 // This is a very basic integration test of modular
@ -72,7 +54,7 @@ func TestIntegration(t *testing.T) {
testdata := analysistest.TestData() 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(), cmd.Env = append(os.Environ(),
"UNITCHECKER_CHILD=1", "UNITCHECKER_CHILD=1",
"GOPATH="+testdata, "GOPATH="+testdata,