mirror of
https://github.com/golang/go
synced 2024-11-14 08:50:22 -07:00
a850dbdef2
Fixes #17148. Change-Id: I4c66aa0733c249ee6019d1c4e802a7e30457d4b6 Reviewed-on: https://go-review.googlesource.com/32030 Run-TryBot: Russ Cox <rsc@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rob Pike <r@golang.org>
511 lines
13 KiB
Go
511 lines
13 KiB
Go
// Copyright 2010 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.
|
|
|
|
// Vet is a simple checker for static errors in Go source code.
|
|
// See doc.go for more information.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/printer"
|
|
"go/token"
|
|
"go/types"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
verbose = flag.Bool("v", false, "verbose")
|
|
tags = flag.String("tags", "", "space-separated list of build tags to apply when parsing")
|
|
tagList = []string{} // exploded version of tags flag; set in main
|
|
)
|
|
|
|
var exitCode = 0
|
|
|
|
// "-all" flag enables all non-experimental checks
|
|
var all = triStateFlag("all", unset, "enable all non-experimental checks")
|
|
|
|
// Flags to control which individual checks to perform.
|
|
var report = map[string]*triState{
|
|
// Only unusual checks are written here.
|
|
// Most checks that operate during the AST walk are added by register.
|
|
"asmdecl": triStateFlag("asmdecl", unset, "check assembly against Go declarations"),
|
|
"buildtags": triStateFlag("buildtags", unset, "check that +build tags are valid"),
|
|
}
|
|
|
|
// experimental records the flags enabling experimental features. These must be
|
|
// requested explicitly; they are not enabled by -all.
|
|
var experimental = map[string]bool{}
|
|
|
|
// setTrueCount record how many flags are explicitly set to true.
|
|
var setTrueCount int
|
|
|
|
// dirsRun and filesRun indicate whether the vet is applied to directory or
|
|
// file targets. The distinction affects which checks are run.
|
|
var dirsRun, filesRun bool
|
|
|
|
// includesNonTest indicates whether the vet is applied to non-test targets.
|
|
// Certain checks are relevant only if they touch both test and non-test files.
|
|
var includesNonTest bool
|
|
|
|
// A triState is a boolean that knows whether it has been set to either true or false.
|
|
// It is used to identify if a flag appears; the standard boolean flag cannot
|
|
// distinguish missing from unset. It also satisfies flag.Value.
|
|
type triState int
|
|
|
|
const (
|
|
unset triState = iota
|
|
setTrue
|
|
setFalse
|
|
)
|
|
|
|
func triStateFlag(name string, value triState, usage string) *triState {
|
|
flag.Var(&value, name, usage)
|
|
return &value
|
|
}
|
|
|
|
// triState implements flag.Value, flag.Getter, and flag.boolFlag.
|
|
// They work like boolean flags: we can say vet -printf as well as vet -printf=true
|
|
func (ts *triState) Get() interface{} {
|
|
return *ts == setTrue
|
|
}
|
|
|
|
func (ts triState) isTrue() bool {
|
|
return ts == setTrue
|
|
}
|
|
|
|
func (ts *triState) Set(value string) error {
|
|
b, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if b {
|
|
*ts = setTrue
|
|
setTrueCount++
|
|
} else {
|
|
*ts = setFalse
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ts *triState) String() string {
|
|
switch *ts {
|
|
case unset:
|
|
return "true" // An unset flag will be set by -all, so defaults to true.
|
|
case setTrue:
|
|
return "true"
|
|
case setFalse:
|
|
return "false"
|
|
}
|
|
panic("not reached")
|
|
}
|
|
|
|
func (ts triState) IsBoolFlag() bool {
|
|
return true
|
|
}
|
|
|
|
// vet tells whether to report errors for the named check, a flag name.
|
|
func vet(name string) bool {
|
|
return report[name].isTrue()
|
|
}
|
|
|
|
// setExit sets the value for os.Exit when it is called, later. It
|
|
// remembers the highest value.
|
|
func setExit(err int) {
|
|
if err > exitCode {
|
|
exitCode = err
|
|
}
|
|
}
|
|
|
|
var (
|
|
// Each of these vars has a corresponding case in (*File).Visit.
|
|
assignStmt *ast.AssignStmt
|
|
binaryExpr *ast.BinaryExpr
|
|
callExpr *ast.CallExpr
|
|
compositeLit *ast.CompositeLit
|
|
exprStmt *ast.ExprStmt
|
|
funcDecl *ast.FuncDecl
|
|
funcLit *ast.FuncLit
|
|
genDecl *ast.GenDecl
|
|
interfaceType *ast.InterfaceType
|
|
rangeStmt *ast.RangeStmt
|
|
returnStmt *ast.ReturnStmt
|
|
structType *ast.StructType
|
|
|
|
// checkers is a two-level map.
|
|
// The outer level is keyed by a nil pointer, one of the AST vars above.
|
|
// The inner level is keyed by checker name.
|
|
checkers = make(map[ast.Node]map[string]func(*File, ast.Node))
|
|
)
|
|
|
|
func register(name, usage string, fn func(*File, ast.Node), types ...ast.Node) {
|
|
report[name] = triStateFlag(name, unset, usage)
|
|
for _, typ := range types {
|
|
m := checkers[typ]
|
|
if m == nil {
|
|
m = make(map[string]func(*File, ast.Node))
|
|
checkers[typ] = m
|
|
}
|
|
m[name] = fn
|
|
}
|
|
}
|
|
|
|
// Usage is a replacement usage function for the flags package.
|
|
func Usage() {
|
|
fmt.Fprintf(os.Stderr, "Usage of vet:\n")
|
|
fmt.Fprintf(os.Stderr, "\tvet [flags] directory...\n")
|
|
fmt.Fprintf(os.Stderr, "\tvet [flags] files... # Must be a single package\n")
|
|
fmt.Fprintf(os.Stderr, "By default, -all is set and all non-experimental checks are run.\n")
|
|
fmt.Fprintf(os.Stderr, "For more information run\n")
|
|
fmt.Fprintf(os.Stderr, "\tgo doc cmd/vet\n\n")
|
|
fmt.Fprintf(os.Stderr, "Flags:\n")
|
|
flag.PrintDefaults()
|
|
os.Exit(2)
|
|
}
|
|
|
|
// File is a wrapper for the state of a file used in the parser.
|
|
// The parse tree walkers are all methods of this type.
|
|
type File struct {
|
|
pkg *Package
|
|
fset *token.FileSet
|
|
name string
|
|
content []byte
|
|
file *ast.File
|
|
b bytes.Buffer // for use by methods
|
|
|
|
// Parsed package "foo" when checking package "foo_test"
|
|
basePkg *Package
|
|
|
|
// The objects that are receivers of a "String() string" method.
|
|
// This is used by the recursiveStringer method in print.go.
|
|
stringers map[*ast.Object]bool
|
|
|
|
// Registered checkers to run.
|
|
checkers map[ast.Node][]func(*File, ast.Node)
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = Usage
|
|
flag.Parse()
|
|
|
|
// If any flag is set, we run only those checks requested.
|
|
// If all flag is set true or if no flags are set true, set all the non-experimental ones
|
|
// not explicitly set (in effect, set the "-all" flag).
|
|
if setTrueCount == 0 || *all == setTrue {
|
|
for name, setting := range report {
|
|
if *setting == unset && !experimental[name] {
|
|
*setting = setTrue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Accept space-separated tags because that matches
|
|
// the go command's other subcommands.
|
|
// Accept commas because go tool vet traditionally has.
|
|
tagList = strings.Fields(strings.Replace(*tags, ",", " ", -1))
|
|
|
|
initPrintFlags()
|
|
initUnusedFlags()
|
|
|
|
if flag.NArg() == 0 {
|
|
Usage()
|
|
}
|
|
for _, name := range flag.Args() {
|
|
// Is it a directory?
|
|
fi, err := os.Stat(name)
|
|
if err != nil {
|
|
warnf("error walking tree: %s", err)
|
|
continue
|
|
}
|
|
if fi.IsDir() {
|
|
dirsRun = true
|
|
} else {
|
|
filesRun = true
|
|
if !strings.HasSuffix(name, "_test.go") {
|
|
includesNonTest = true
|
|
}
|
|
}
|
|
}
|
|
if dirsRun && filesRun {
|
|
Usage()
|
|
}
|
|
if dirsRun {
|
|
for _, name := range flag.Args() {
|
|
walkDir(name)
|
|
}
|
|
os.Exit(exitCode)
|
|
}
|
|
if doPackage(".", flag.Args(), nil) == nil {
|
|
warnf("no files checked")
|
|
}
|
|
os.Exit(exitCode)
|
|
}
|
|
|
|
// prefixDirectory places the directory name on the beginning of each name in the list.
|
|
func prefixDirectory(directory string, names []string) {
|
|
if directory != "." {
|
|
for i, name := range names {
|
|
names[i] = filepath.Join(directory, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// doPackageDir analyzes the single package found in the directory, if there is one,
|
|
// plus a test package, if there is one.
|
|
func doPackageDir(directory string) {
|
|
context := build.Default
|
|
if len(context.BuildTags) != 0 {
|
|
warnf("build tags %s previously set", context.BuildTags)
|
|
}
|
|
context.BuildTags = append(tagList, context.BuildTags...)
|
|
|
|
pkg, err := context.ImportDir(directory, 0)
|
|
if err != nil {
|
|
// If it's just that there are no go source files, that's fine.
|
|
if _, nogo := err.(*build.NoGoError); nogo {
|
|
return
|
|
}
|
|
// Non-fatal: we are doing a recursive walk and there may be other directories.
|
|
warnf("cannot process directory %s: %s", directory, err)
|
|
return
|
|
}
|
|
var names []string
|
|
names = append(names, pkg.GoFiles...)
|
|
names = append(names, pkg.CgoFiles...)
|
|
names = append(names, pkg.TestGoFiles...) // These are also in the "foo" package.
|
|
names = append(names, pkg.SFiles...)
|
|
prefixDirectory(directory, names)
|
|
basePkg := doPackage(directory, names, nil)
|
|
// Is there also a "foo_test" package? If so, do that one as well.
|
|
if len(pkg.XTestGoFiles) > 0 {
|
|
names = pkg.XTestGoFiles
|
|
prefixDirectory(directory, names)
|
|
doPackage(directory, names, basePkg)
|
|
}
|
|
}
|
|
|
|
type Package struct {
|
|
path string
|
|
defs map[*ast.Ident]types.Object
|
|
uses map[*ast.Ident]types.Object
|
|
selectors map[*ast.SelectorExpr]*types.Selection
|
|
types map[ast.Expr]types.TypeAndValue
|
|
spans map[types.Object]Span
|
|
files []*File
|
|
typesPkg *types.Package
|
|
}
|
|
|
|
// doPackage analyzes the single package constructed from the named files.
|
|
// It returns the parsed Package or nil if none of the files have been checked.
|
|
func doPackage(directory string, names []string, basePkg *Package) *Package {
|
|
var files []*File
|
|
var astFiles []*ast.File
|
|
fs := token.NewFileSet()
|
|
for _, name := range names {
|
|
data, err := ioutil.ReadFile(name)
|
|
if err != nil {
|
|
// Warn but continue to next package.
|
|
warnf("%s: %s", name, err)
|
|
return nil
|
|
}
|
|
checkBuildTag(name, data)
|
|
var parsedFile *ast.File
|
|
if strings.HasSuffix(name, ".go") {
|
|
parsedFile, err = parser.ParseFile(fs, name, data, 0)
|
|
if err != nil {
|
|
warnf("%s: %s", name, err)
|
|
return nil
|
|
}
|
|
astFiles = append(astFiles, parsedFile)
|
|
}
|
|
files = append(files, &File{fset: fs, content: data, name: name, file: parsedFile})
|
|
}
|
|
if len(astFiles) == 0 {
|
|
return nil
|
|
}
|
|
pkg := new(Package)
|
|
pkg.path = astFiles[0].Name.Name
|
|
pkg.files = files
|
|
// Type check the package.
|
|
err := pkg.check(fs, astFiles)
|
|
if err != nil && *verbose {
|
|
warnf("%s", err)
|
|
}
|
|
|
|
// Check.
|
|
chk := make(map[ast.Node][]func(*File, ast.Node))
|
|
for typ, set := range checkers {
|
|
for name, fn := range set {
|
|
if vet(name) {
|
|
chk[typ] = append(chk[typ], fn)
|
|
}
|
|
}
|
|
}
|
|
for _, file := range files {
|
|
file.pkg = pkg
|
|
file.basePkg = basePkg
|
|
file.checkers = chk
|
|
if file.file != nil {
|
|
file.walkFile(file.name, file.file)
|
|
}
|
|
}
|
|
asmCheck(pkg)
|
|
return pkg
|
|
}
|
|
|
|
func visit(path string, f os.FileInfo, err error) error {
|
|
if err != nil {
|
|
warnf("walk error: %s", err)
|
|
return err
|
|
}
|
|
// One package per directory. Ignore the files themselves.
|
|
if !f.IsDir() {
|
|
return nil
|
|
}
|
|
doPackageDir(path)
|
|
return nil
|
|
}
|
|
|
|
func (pkg *Package) hasFileWithSuffix(suffix string) bool {
|
|
for _, f := range pkg.files {
|
|
if strings.HasSuffix(f.name, suffix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// walkDir recursively walks the tree looking for Go packages.
|
|
func walkDir(root string) {
|
|
filepath.Walk(root, visit)
|
|
}
|
|
|
|
// errorf formats the error to standard error, adding program
|
|
// identification and a newline, and exits.
|
|
func errorf(format string, args ...interface{}) {
|
|
fmt.Fprintf(os.Stderr, "vet: "+format+"\n", args...)
|
|
os.Exit(2)
|
|
}
|
|
|
|
// warnf formats the error to standard error, adding program
|
|
// identification and a newline, but does not exit.
|
|
func warnf(format string, args ...interface{}) {
|
|
fmt.Fprintf(os.Stderr, "vet: "+format+"\n", args...)
|
|
setExit(1)
|
|
}
|
|
|
|
// Println is fmt.Println guarded by -v.
|
|
func Println(args ...interface{}) {
|
|
if !*verbose {
|
|
return
|
|
}
|
|
fmt.Println(args...)
|
|
}
|
|
|
|
// Printf is fmt.Printf guarded by -v.
|
|
func Printf(format string, args ...interface{}) {
|
|
if !*verbose {
|
|
return
|
|
}
|
|
fmt.Printf(format+"\n", args...)
|
|
}
|
|
|
|
// Bad reports an error and sets the exit code..
|
|
func (f *File) Bad(pos token.Pos, args ...interface{}) {
|
|
f.Warn(pos, args...)
|
|
setExit(1)
|
|
}
|
|
|
|
// Badf reports a formatted error and sets the exit code.
|
|
func (f *File) Badf(pos token.Pos, format string, args ...interface{}) {
|
|
f.Warnf(pos, format, args...)
|
|
setExit(1)
|
|
}
|
|
|
|
// loc returns a formatted representation of the position.
|
|
func (f *File) loc(pos token.Pos) string {
|
|
if pos == token.NoPos {
|
|
return ""
|
|
}
|
|
// Do not print columns. Because the pos often points to the start of an
|
|
// expression instead of the inner part with the actual error, the
|
|
// precision can mislead.
|
|
posn := f.fset.Position(pos)
|
|
return fmt.Sprintf("%s:%d", posn.Filename, posn.Line)
|
|
}
|
|
|
|
// locPrefix returns a formatted representation of the position for use as a line prefix.
|
|
func (f *File) locPrefix(pos token.Pos) string {
|
|
if pos == token.NoPos {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%s: ", f.loc(pos))
|
|
}
|
|
|
|
// Warn reports an error but does not set the exit code.
|
|
func (f *File) Warn(pos token.Pos, args ...interface{}) {
|
|
fmt.Fprintf(os.Stderr, "%s%s", f.locPrefix(pos), fmt.Sprintln(args...))
|
|
}
|
|
|
|
// Warnf reports a formatted error but does not set the exit code.
|
|
func (f *File) Warnf(pos token.Pos, format string, args ...interface{}) {
|
|
fmt.Fprintf(os.Stderr, "%s%s\n", f.locPrefix(pos), fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// walkFile walks the file's tree.
|
|
func (f *File) walkFile(name string, file *ast.File) {
|
|
Println("Checking file", name)
|
|
ast.Walk(f, file)
|
|
}
|
|
|
|
// Visit implements the ast.Visitor interface.
|
|
func (f *File) Visit(node ast.Node) ast.Visitor {
|
|
var key ast.Node
|
|
switch node.(type) {
|
|
case *ast.AssignStmt:
|
|
key = assignStmt
|
|
case *ast.BinaryExpr:
|
|
key = binaryExpr
|
|
case *ast.CallExpr:
|
|
key = callExpr
|
|
case *ast.CompositeLit:
|
|
key = compositeLit
|
|
case *ast.ExprStmt:
|
|
key = exprStmt
|
|
case *ast.FuncDecl:
|
|
key = funcDecl
|
|
case *ast.FuncLit:
|
|
key = funcLit
|
|
case *ast.GenDecl:
|
|
key = genDecl
|
|
case *ast.InterfaceType:
|
|
key = interfaceType
|
|
case *ast.RangeStmt:
|
|
key = rangeStmt
|
|
case *ast.ReturnStmt:
|
|
key = returnStmt
|
|
case *ast.StructType:
|
|
key = structType
|
|
}
|
|
for _, fn := range f.checkers[key] {
|
|
fn(f, node)
|
|
}
|
|
return f
|
|
}
|
|
|
|
// gofmt returns a string representation of the expression.
|
|
func (f *File) gofmt(x ast.Expr) string {
|
|
f.b.Reset()
|
|
printer.Fprint(&f.b, f.fset, x)
|
|
return f.b.String()
|
|
}
|