go/analysis/internal/unitchecker: a 'go vet'-compatible driver
Also:
- add cmd/vet-lite, a version of cmd/vet that doesn't depend on
go/packages and must be run under "go vet". This will be vendored
into $GOROOT/src/cmd/vet.
- add an integration test for a unitchecker-based command under "go vet".
Change-Id: Id613dac2812816c6d6372fa6d1536c8d4e4c2676
Reviewed-on: https://go-review.googlesource.com/c/143418
Reviewed-by: Ian Cottrell <iancottrell@google.com>
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
new file mode 100644
index 0000000..ae66a7d
--- /dev/null
+++ b/go/analysis/cmd/vet-lite/main.go
@@ -0,0 +1,83 @@
+// The vet-lite command is a driver for static checkers conforming to
+// the golang.org/x/tools/go/analysis API. It must be run by go vet:
+//
+// $ GOVETTOOL=$(which vet-lite) go vet
+//
+// For a checker also capable of running standalone, use multichecker.
+package main
+
+import (
+ "flag"
+ "log"
+ "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/passes/asmdecl"
+ "golang.org/x/tools/go/analysis/passes/assign"
+ "golang.org/x/tools/go/analysis/passes/atomic"
+ "golang.org/x/tools/go/analysis/passes/bools"
+ "golang.org/x/tools/go/analysis/passes/buildtag"
+ "golang.org/x/tools/go/analysis/passes/cgocall"
+ "golang.org/x/tools/go/analysis/passes/composite"
+ "golang.org/x/tools/go/analysis/passes/copylock"
+ "golang.org/x/tools/go/analysis/passes/httpresponse"
+ "golang.org/x/tools/go/analysis/passes/loopclosure"
+ "golang.org/x/tools/go/analysis/passes/lostcancel"
+ "golang.org/x/tools/go/analysis/passes/nilfunc"
+ "golang.org/x/tools/go/analysis/passes/pkgfact"
+ "golang.org/x/tools/go/analysis/passes/printf"
+ "golang.org/x/tools/go/analysis/passes/shift"
+ "golang.org/x/tools/go/analysis/passes/stdmethods"
+ "golang.org/x/tools/go/analysis/passes/structtag"
+ "golang.org/x/tools/go/analysis/passes/tests"
+ "golang.org/x/tools/go/analysis/passes/unreachable"
+ "golang.org/x/tools/go/analysis/passes/unsafeptr"
+ "golang.org/x/tools/go/analysis/passes/unusedresult"
+)
+
+var analyzers = []*analysis.Analyzer{
+ // For now, just the traditional vet suite:
+ asmdecl.Analyzer,
+ assign.Analyzer,
+ atomic.Analyzer,
+ bools.Analyzer,
+ buildtag.Analyzer,
+ cgocall.Analyzer,
+ composite.Analyzer,
+ copylock.Analyzer,
+ httpresponse.Analyzer,
+ loopclosure.Analyzer,
+ lostcancel.Analyzer,
+ nilfunc.Analyzer,
+ pkgfact.Analyzer,
+ printf.Analyzer,
+ // shadow.Analyzer, // experimental; not enabled by default
+ shift.Analyzer,
+ stdmethods.Analyzer,
+ structtag.Analyzer,
+ tests.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)
+ }
+
+ analyzers = analysisflags.Parse(analyzers, true)
+
+ args := flag.Args()
+ 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)
+}
diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go
index a7c7826..e0c1029 100644
--- a/go/analysis/internal/checker/checker.go
+++ b/go/analysis/internal/checker/checker.go
@@ -25,6 +25,7 @@
"time"
"golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/internal/unitchecker"
"golang.org/x/tools/go/packages"
)
@@ -106,6 +107,18 @@
}()
}
+ // 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("")
diff --git a/go/analysis/internal/facts/facts.go b/go/analysis/internal/facts/facts.go
index c1edc90..468f148 100644
--- a/go/analysis/internal/facts/facts.go
+++ b/go/analysis/internal/facts/facts.go
@@ -217,7 +217,7 @@
s.mu.Lock()
for k, fact := range s.m {
if debug {
- log.Printf("%#v => %s\n", k, fact)
+ log.Printf("%v => %s\n", k, fact)
}
var object objectpath.Path
if k.obj != nil {
@@ -259,15 +259,6 @@
if len(gobFacts) > 0 {
if err := gob.NewEncoder(&buf).Encode(gobFacts); err != nil {
// Fact encoding should never fail. Identify the culprit.
- //
- // TODO(adonovan): what's the right thing to do here?
- // The error is clearly a bug, so log.Fatal leads to early
- // detection, but it could potentially bring down a big
- // job because of an obscure dynamic bug in a fact.
- // But perhaps that's fine: other bugs in Analyzers
- // have the same potential to cause failures.
- // Alternatively we could discard the bad facts with a
- // log message, but who reads logs?
for _, gf := range gobFacts {
if err := gob.NewEncoder(ioutil.Discard).Encode(gf); err != nil {
fact := gf.Fact
diff --git a/go/analysis/internal/unitchecker/testdata/src/a/a.go b/go/analysis/internal/unitchecker/testdata/src/a/a.go
new file mode 100644
index 0000000..9765e19
--- /dev/null
+++ b/go/analysis/internal/unitchecker/testdata/src/a/a.go
@@ -0,0 +1,7 @@
+package a
+
+func _() {
+ MyFunc123()
+}
+
+func MyFunc123() {}
diff --git a/go/analysis/internal/unitchecker/testdata/src/b/b.go b/go/analysis/internal/unitchecker/testdata/src/b/b.go
new file mode 100644
index 0000000..a7b6125
--- /dev/null
+++ b/go/analysis/internal/unitchecker/testdata/src/b/b.go
@@ -0,0 +1,10 @@
+package b
+
+import "a"
+
+func _() {
+ a.MyFunc123()
+ MyFunc123()
+}
+
+func MyFunc123() {}
diff --git a/go/analysis/internal/unitchecker/unitchecker.go b/go/analysis/internal/unitchecker/unitchecker.go
new file mode 100644
index 0000000..b67c943
--- /dev/null
+++ b/go/analysis/internal/unitchecker/unitchecker.go
@@ -0,0 +1,306 @@
+// 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.
+
+// The unitchecker package defines the main function for an analysis
+// driver that analyzes a single compilation unit during a build.
+// It is invoked by a build system such as "go vet":
+//
+// $ GOVETTOOL=$(which vet) go vet
+//
+// It supports the following command-line protocol:
+//
+// -V=full describe executable (to the build tool)
+// -flags describe flags (to the build tool)
+// foo.cfg description of compilation unit (from the build tool)
+//
+// This package does not depend on go/packages.
+// If you need a standalone tool, use multichecker,
+// which supports this mode but can also load packages
+// from source using go/packages.
+package unitchecker
+
+// TODO(adonovan):
+// - with gccgo, go build does not build standard library,
+// 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"
+ "encoding/json"
+ "fmt"
+ "go/ast"
+ "go/build"
+ "go/importer"
+ "go/parser"
+ "go/token"
+ "go/types"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/internal/facts"
+)
+
+// A Config describes a compilation unit to be analyzed.
+// It is provided to the tool in a JSON-encoded file
+// whose name ends with ".cfg".
+type Config struct {
+ Compiler string
+ Dir string
+ ImportPath string
+ GoFiles []string
+ OtherFiles []string // TODO(adonovan): make go vet populate this (github.com/golang/go/issues/27665)
+ ImportMap map[string]string
+ PackageFile map[string]string
+ Standard map[string]bool
+ PackageVetx map[string]string
+ VetxOnly bool
+ VetxOutput string
+ SucceedOnTypecheckFailure bool
+}
+
+// Main reads the *.cfg file, runs the analysis,
+// and calls os.Exit with an appropriate error code.
+func Main(configFile string, analyzers []*analysis.Analyzer) {
+ cfg, err := readConfig(configFile)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fset := token.NewFileSet()
+ diags, err := run(fset, cfg, analyzers)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if len(diags) > 0 {
+ for _, diag := range diags {
+ fmt.Fprintf(os.Stderr, "%s: %s\n", fset.Position(diag.Pos), diag.Message)
+ }
+ os.Exit(1)
+ }
+
+ os.Exit(0)
+}
+
+func readConfig(filename string) (*Config, error) {
+ data, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ cfg := new(Config)
+ if err := json.Unmarshal(data, cfg); err != nil {
+ return nil, fmt.Errorf("cannot decode JSON config file %s: %v", filename, err)
+ }
+ if len(cfg.GoFiles) == 0 {
+ // The go command disallows packages with no files.
+ // The only exception is unsafe, but the go command
+ // doesn't call vet on it.
+ return nil, fmt.Errorf("package has no files: %s", cfg.ImportPath)
+ }
+ return cfg, nil
+}
+
+func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]analysis.Diagnostic, error) {
+ // Load, parse, typecheck.
+ var files []*ast.File
+ for _, name := range cfg.GoFiles {
+ f, err := parser.ParseFile(fset, name, nil, parser.ParseComments)
+ if err != nil {
+ if cfg.SucceedOnTypecheckFailure {
+ // Silently succeed; let the compiler
+ // report parse errors.
+ err = nil
+ }
+ return nil, err
+ }
+ files = append(files, f)
+ }
+ compilerImporter := importer.For(cfg.Compiler, func(path string) (io.ReadCloser, error) {
+ // path is a resolved package path, not an import path.
+ file, ok := cfg.PackageFile[path]
+ if !ok {
+ if cfg.Compiler == "gccgo" && cfg.Standard[path] {
+ return nil, nil // fall back to default gccgo lookup
+ }
+ return nil, fmt.Errorf("no package file for %q", path)
+ }
+ return os.Open(file)
+ })
+ importer := importerFunc(func(importPath string) (*types.Package, error) {
+ path, ok := cfg.ImportMap[importPath] // resolve vendoring, etc
+ if !ok {
+ return nil, fmt.Errorf("can't resolve import %q", path)
+ }
+ return compilerImporter.Import(path)
+ })
+ tc := &types.Config{
+ Importer: importer,
+ Sizes: types.SizesFor("gc", build.Default.GOARCH), // assume gccgo ≡ gc?
+ }
+ info := &types.Info{
+ Types: make(map[ast.Expr]types.TypeAndValue),
+ Defs: make(map[*ast.Ident]types.Object),
+ Uses: make(map[*ast.Ident]types.Object),
+ Implicits: make(map[ast.Node]types.Object),
+ Scopes: make(map[ast.Node]*types.Scope),
+ Selections: make(map[*ast.SelectorExpr]*types.Selection),
+ }
+ pkg, err := tc.Check(cfg.ImportPath, fset, files, info)
+ if err != nil {
+ if cfg.SucceedOnTypecheckFailure {
+ // Silently succeed; let the compiler
+ // report type errors.
+ err = nil
+ }
+ return nil, err
+ }
+
+ // Register fact types with gob.
+ // In VetxOnly mode, analyzers are only for their facts,
+ // so we can skip any analysis that neither produces facts
+ // nor depends on any analysis that produces facts.
+ // Also build a map to hold working state and result.
+ type action struct {
+ once sync.Once
+ result interface{}
+ err error
+ usesFacts bool // (transitively uses)
+ diagnostics []analysis.Diagnostic
+ }
+ actions := make(map[*analysis.Analyzer]*action)
+ var registerFacts func(a *analysis.Analyzer) bool
+ registerFacts = func(a *analysis.Analyzer) bool {
+ act, ok := actions[a]
+ if !ok {
+ act = new(action)
+ var usesFacts bool
+ for _, f := range a.FactTypes {
+ usesFacts = true
+ gob.Register(f)
+ }
+ for _, req := range a.Requires {
+ if registerFacts(req) {
+ usesFacts = true
+ }
+ }
+ act.usesFacts = usesFacts
+ actions[a] = act
+ }
+ return act.usesFacts
+ }
+ var filtered []*analysis.Analyzer
+ for _, a := range analyzers {
+ if registerFacts(a) || !cfg.VetxOnly {
+ filtered = append(filtered, a)
+ }
+ }
+ analyzers = filtered
+
+ // Read facts from imported packages.
+ read := func(path string) ([]byte, error) {
+ if vetx, ok := cfg.PackageVetx[path]; ok {
+ return ioutil.ReadFile(vetx)
+ }
+ return nil, nil // no .vetx file, no facts
+ }
+ facts, err := facts.Decode(pkg, read)
+ if err != nil {
+ return nil, err
+ }
+
+ // In parallel, execute the DAG of analyzers.
+ var exec func(a *analysis.Analyzer) *action
+ var execAll func(analyzers []*analysis.Analyzer)
+ exec = func(a *analysis.Analyzer) *action {
+ act := actions[a]
+ act.once.Do(func() {
+ execAll(a.Requires) // prefetch dependencies in parallel
+
+ // The inputs to this analysis are the
+ // results of its prerequisites.
+ inputs := make(map[*analysis.Analyzer]interface{})
+ var failed []string
+ for _, req := range a.Requires {
+ reqact := exec(req)
+ if reqact.err != nil {
+ failed = append(failed, req.String())
+ continue
+ }
+ inputs[req] = reqact.result
+ }
+
+ // Report an error if any dependency failed.
+ if failed != nil {
+ sort.Strings(failed)
+ act.err = fmt.Errorf("failed prerequisites: %s", strings.Join(failed, ", "))
+ return
+ }
+
+ pass := &analysis.Pass{
+ Analyzer: a,
+ Fset: fset,
+ Files: files,
+ OtherFiles: cfg.OtherFiles,
+ Pkg: pkg,
+ TypesInfo: info,
+ ResultOf: inputs,
+ Report: func(d analysis.Diagnostic) { act.diagnostics = append(act.diagnostics, d) },
+ ImportObjectFact: facts.ImportObjectFact,
+ ExportObjectFact: facts.ExportObjectFact,
+ ImportPackageFact: facts.ImportPackageFact,
+ ExportPackageFact: facts.ExportPackageFact,
+ }
+
+ t0 := time.Now()
+ act.result, act.err = a.Run(pass)
+ if false {
+ log.Printf("analysis %s = %s", pass, time.Since(t0))
+ }
+ })
+ return act
+ }
+ execAll = func(analyzers []*analysis.Analyzer) {
+ var wg sync.WaitGroup
+ for _, a := range analyzers {
+ wg.Add(1)
+ go func(a *analysis.Analyzer) {
+ _ = exec(a)
+ wg.Done()
+ }(a)
+ }
+ wg.Wait()
+ }
+
+ execAll(analyzers)
+
+ // Return diagnostics from root analyzers.
+ var diags []analysis.Diagnostic
+ for _, a := range analyzers {
+ act := actions[a]
+ if act.err != nil {
+ return nil, act.err // some analysis failed
+ }
+ diags = append(diags, act.diagnostics...)
+ }
+
+ data := facts.Encode()
+ if err := ioutil.WriteFile(cfg.VetxOutput, data, 0666); err != nil {
+ return nil, fmt.Errorf("failed to write analysis facts: %v", err)
+ }
+
+ return diags, nil
+}
+
+type importerFunc func(path string) (*types.Package, error)
+
+func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }
diff --git a/go/analysis/internal/unitchecker/unitchecker_test.go b/go/analysis/internal/unitchecker/unitchecker_test.go
new file mode 100644
index 0000000..872275f
--- /dev/null
+++ b/go/analysis/internal/unitchecker/unitchecker_test.go
@@ -0,0 +1,97 @@
+// 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 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"
+)
+
+func TestMain(m *testing.M) {
+ if os.Getenv("UNITCHECKER_CHILD") == "1" {
+ // child process
+ main()
+ panic("unreachable")
+ }
+
+ // test
+ flag.Parse()
+ os.Exit(m.Run())
+}
+
+func main() {
+ findcall.Analyzer.Flags.Set("name", "MyFunc123")
+
+ var analyzers = []*analysis.Analyzer{
+ 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
+// analysis with facts using unitchecker under "go vet".
+// It fork/execs the main function above.
+func TestIntegration(t *testing.T) {
+ if runtime.GOOS != "linux" {
+ t.Skipf("skipping fork/exec test on this platform")
+ }
+
+ testdata := analysistest.TestData()
+
+ cmd := exec.Command("go", "vet", "b")
+ cmd.Env = append(os.Environ(),
+ "GOVETTOOL="+os.Args[0],
+ "UNITCHECKER_CHILD=1",
+ "GOPATH="+testdata,
+ )
+
+ out, err := cmd.CombinedOutput()
+ exitcode := -1
+ if exitErr, ok := err.(*exec.ExitError); ok {
+ exitcode = exitErr.ExitCode()
+ }
+ if exitcode != 2 {
+ t.Errorf("got exit code %d, want 2", exitcode)
+ }
+
+ want := `
+# a
+testdata/src/a/a.go:4:11: call of MyFunc123(...)
+# b
+testdata/src/b/b.go:6:13: call of MyFunc123(...)
+testdata/src/b/b.go:7:11: call of MyFunc123(...)
+`[1:]
+ if got := string(out); got != want {
+ t.Errorf("got <<%s>>, want <<%s>>", got, want)
+ }
+
+ if t.Failed() {
+ t.Logf("err=%v stderr=<<%s>", err, cmd.Stderr)
+ }
+}
diff --git a/go/analysis/multichecker/multichecker.go b/go/analysis/multichecker/multichecker.go
index 27406fa..9038eac 100644
--- a/go/analysis/multichecker/multichecker.go
+++ b/go/analysis/multichecker/multichecker.go
@@ -41,7 +41,7 @@
func Main(analyzers ...*analysis.Analyzer) {
progname := filepath.Base(os.Args[0])
log.SetFlags(0)
- log.SetPrefix(progname + ": ")
+ log.SetPrefix(filepath.Base(os.Args[0]) + ": ") // e.g. "vet: "
if err := analysis.Validate(analyzers); err != nil {
log.Fatal(err)
diff --git a/go/analysis/passes/pkgfact/pkgfact.go b/go/analysis/passes/pkgfact/pkgfact.go
index eca0f25..e053086 100644
--- a/go/analysis/passes/pkgfact/pkgfact.go
+++ b/go/analysis/passes/pkgfact/pkgfact.go
@@ -70,14 +70,14 @@
}
}
- // At each "const _name = value", add a fact into env.
+ // At each "const _name_ = value", add a fact into env.
doConst := func(spec *ast.ValueSpec) {
if len(spec.Names) == len(spec.Values) {
for i := range spec.Names {
name := spec.Names[i].Name
if strings.HasPrefix(name, "_") && strings.HasSuffix(name, "_") {
- if key := strings.Trim(name[1:], "_"); key != "" {
+ if key := strings.Trim(name, "_"); key != "" {
value := pass.TypesInfo.Types[spec.Values[i]].Value.String()
result[key] = value
}