1
0
mirror of https://github.com/golang/go synced 2024-11-14 15:00:27 -07:00
go/src/cmd/vet/main.go

511 lines
13 KiB
Go
Raw Normal View History

// 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()
}