397 lines
11 KiB
Go
397 lines
11 KiB
Go
|
// 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":
|
||
|
//
|
||
|
// $ go vet -vettool=$(which 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.
|
||
|
|
||
|
import (
|
||
|
"encoding/gob"
|
||
|
"encoding/json"
|
||
|
"flag"
|
||
|
"fmt"
|
||
|
"go/ast"
|
||
|
"go/build"
|
||
|
"go/importer"
|
||
|
"go/parser"
|
||
|
"go/token"
|
||
|
"go/types"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"log"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"reflect"
|
||
|
"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"
|
||
|
)
|
||
|
|
||
|
// 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 {
|
||
|
ID string // e.g. "fmt [fmt.test]"
|
||
|
Compiler string
|
||
|
Dir string
|
||
|
ImportPath string
|
||
|
GoFiles []string
|
||
|
NonGoFiles []string
|
||
|
ImportMap map[string]string
|
||
|
PackageFile map[string]string
|
||
|
Standard map[string]bool
|
||
|
PackageVetx map[string]string
|
||
|
VetxOnly bool
|
||
|
VetxOutput string
|
||
|
SucceedOnTypecheckFailure bool
|
||
|
}
|
||
|
|
||
|
// 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(`invoking "go tool vet" directly is unsupported; use "go vet"`)
|
||
|
}
|
||
|
Run(args[0], analyzers)
|
||
|
}
|
||
|
|
||
|
// Run reads the *.cfg file, runs the analysis,
|
||
|
// and calls os.Exit with an appropriate error code.
|
||
|
// It assumes flags have already been set.
|
||
|
func Run(configFile string, analyzers []*analysis.Analyzer) {
|
||
|
cfg, err := readConfig(configFile)
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
fset := token.NewFileSet()
|
||
|
results, err := run(fset, cfg, analyzers)
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// 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(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
|
||
|
}
|
||
|
|
||
|
var importerForCompiler = func(_ *token.FileSet, compiler string, lookup importer.Lookup) types.Importer {
|
||
|
// broken legacy implementation (https://golang.org/issue/28995)
|
||
|
return importer.For(compiler, lookup)
|
||
|
}
|
||
|
|
||
|
func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]result, 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 := importerForCompiler(fset, 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
|
||
|
}
|
||
|
|
||
|
factFilter := make(map[reflect.Type]bool)
|
||
|
for _, f := range a.FactTypes {
|
||
|
factFilter[reflect.TypeOf(f)] = true
|
||
|
}
|
||
|
|
||
|
pass := &analysis.Pass{
|
||
|
Analyzer: a,
|
||
|
Fset: fset,
|
||
|
Files: files,
|
||
|
OtherFiles: cfg.NonGoFiles,
|
||
|
Pkg: pkg,
|
||
|
TypesInfo: info,
|
||
|
TypesSizes: tc.Sizes,
|
||
|
ResultOf: inputs,
|
||
|
Report: func(d analysis.Diagnostic) { act.diagnostics = append(act.diagnostics, d) },
|
||
|
ImportObjectFact: facts.ImportObjectFact,
|
||
|
ExportObjectFact: facts.ExportObjectFact,
|
||
|
AllObjectFacts: func() []analysis.ObjectFact { return facts.AllObjectFacts(factFilter) },
|
||
|
ImportPackageFact: facts.ImportPackageFact,
|
||
|
ExportPackageFact: facts.ExportPackageFact,
|
||
|
AllPackageFacts: func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) },
|
||
|
}
|
||
|
|
||
|
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 and errors from root analyzers.
|
||
|
results := make([]result, len(analyzers))
|
||
|
for i, a := range analyzers {
|
||
|
act := actions[a]
|
||
|
results[i].a = a
|
||
|
results[i].err = act.err
|
||
|
results[i].diagnostics = 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 results, nil
|
||
|
}
|
||
|
|
||
|
type result struct {
|
||
|
a *analysis.Analyzer
|
||
|
diagnostics []analysis.Diagnostic
|
||
|
err error
|
||
|
}
|
||
|
|
||
|
type importerFunc func(path string) (*types.Package, error)
|
||
|
|
||
|
func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }
|