blob: 6369927fa579433bb82d06dcea08a01f31820c6b [file] [log] [blame] [edit]
package main
import (
"bufio"
"bytes"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"github.com/kisielk/errcheck/errcheck"
"golang.org/x/tools/go/packages"
)
const (
exitCodeOk int = iota
exitUncheckedError
exitFatalError
)
type ignoreFlag map[string]*regexp.Regexp
// global flags
var (
abspath bool
verbose bool
)
func (f ignoreFlag) String() string {
pairs := make([]string, 0, len(f))
for pkg, re := range f {
prefix := ""
if pkg != "" {
prefix = pkg + ":"
}
pairs = append(pairs, prefix+re.String())
}
return fmt.Sprintf("%q", strings.Join(pairs, ","))
}
func (f ignoreFlag) Set(s string) error {
if s == "" {
return nil
}
for _, pair := range strings.Split(s, ",") {
colonIndex := strings.Index(pair, ":")
var pkg, re string
if colonIndex == -1 {
pkg = ""
re = pair
} else {
pkg = pair[:colonIndex]
re = pair[colonIndex+1:]
}
regex, err := regexp.Compile(re)
if err != nil {
return err
}
f[pkg] = regex
}
return nil
}
type tagsFlag []string
func (f *tagsFlag) String() string {
return fmt.Sprintf("%q", strings.Join(*f, ","))
}
func (f *tagsFlag) Set(s string) error {
if s == "" {
return nil
}
tags := strings.FieldsFunc(s, func(c rune) bool {
return c == ' ' || c == ','
})
for _, tag := range tags {
if tag != "" {
*f = append(*f, tag)
}
}
return nil
}
func reportResult(e errcheck.Result) {
wd, err := os.Getwd()
if err != nil {
wd = ""
}
for _, uncheckedError := range e.UncheckedErrors {
pos := uncheckedError.Pos.String()
if !abspath {
newPos, err := filepath.Rel(wd, pos)
if err == nil {
pos = newPos
}
}
if verbose && uncheckedError.FuncName != "" {
fmt.Printf("%s:\t%s\t%s\n", pos, uncheckedError.FuncName, uncheckedError.Line)
} else {
fmt.Printf("%s:\t%s\n", pos, uncheckedError.Line)
}
}
}
func logf(msg string, args ...interface{}) {
if verbose {
fmt.Fprintf(os.Stderr, msg+"\n", args...)
}
}
func mainCmd(args []string) int {
var checker errcheck.Checker
paths, rc := parseFlags(&checker, args)
if rc != exitCodeOk {
return rc
}
result, err := checkPaths(&checker, paths...)
if err != nil {
if err == errcheck.ErrNoGoFiles {
fmt.Fprintln(os.Stderr, err)
return exitCodeOk
}
fmt.Fprintf(os.Stderr, "error: failed to check packages: %s\n", err)
return exitFatalError
}
if len(result.UncheckedErrors) > 0 {
reportResult(result)
return exitUncheckedError
}
return exitCodeOk
}
func checkPaths(c *errcheck.Checker, paths ...string) (errcheck.Result, error) {
pkgs, err := c.LoadPackages(paths...)
if err != nil {
return errcheck.Result{}, err
}
// Check for errors in the initial packages.
work := make(chan *packages.Package, len(pkgs))
for _, pkg := range pkgs {
if len(pkg.Errors) > 0 {
return errcheck.Result{}, fmt.Errorf("errors while loading package %s: %v", pkg.ID, pkg.Errors)
}
work <- pkg
}
close(work)
var wg sync.WaitGroup
result := &errcheck.Result{}
mu := &sync.Mutex{}
for i := 0; i < runtime.NumCPU(); i++ {
wg.Add(1)
go func() {
defer wg.Done()
for pkg := range work {
logf("checking %s", pkg.Types.Path())
r := c.CheckPackage(pkg)
mu.Lock()
result.Append(r)
mu.Unlock()
}
}()
}
wg.Wait()
return result.Unique(), nil
}
func parseFlags(checker *errcheck.Checker, args []string) ([]string, int) {
flags := flag.NewFlagSet(args[0], flag.ContinueOnError)
var checkAsserts, checkBlanks bool
flags.BoolVar(&checkBlanks, "blank", false, "if true, check for errors assigned to blank identifier")
flags.BoolVar(&checkAsserts, "asserts", false, "if true, check for ignored type assertion results")
flags.BoolVar(&checker.Exclusions.TestFiles, "ignoretests", false, "if true, checking of _test.go files is disabled")
flags.BoolVar(&checker.Exclusions.GeneratedFiles, "ignoregenerated", false, "if true, checking of files with generated code is disabled")
flags.BoolVar(&verbose, "verbose", false, "produce more verbose logging")
flags.BoolVar(&abspath, "abspath", false, "print absolute paths to files")
tags := tagsFlag{}
flags.Var(&tags, "tags", "comma or space-separated list of build tags to include")
ignorePkg := flags.String("ignorepkg", "", "comma-separated list of package paths to ignore")
ignore := ignoreFlag(map[string]*regexp.Regexp{})
flags.Var(ignore, "ignore", "[deprecated] comma-separated list of pairs of the form pkg:regex\n"+
" the regex is used to ignore names within pkg.")
var excludeFile string
flags.StringVar(&excludeFile, "exclude", "", "Path to a file containing a list of functions to exclude from checking")
var excludeOnly bool
flags.BoolVar(&excludeOnly, "excludeonly", false, "Use only excludes from -exclude file")
if err := flags.Parse(args[1:]); err != nil {
return nil, exitFatalError
}
checker.Exclusions.BlankAssignments = !checkBlanks
checker.Exclusions.TypeAssertions = !checkAsserts
if !excludeOnly {
checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, errcheck.DefaultExcludedSymbols...)
}
if excludeFile != "" {
excludes, err := readExcludes(excludeFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not read exclude file: %v\n", err)
return nil, exitFatalError
}
checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, excludes...)
}
checker.Tags = tags
for _, pkg := range strings.Split(*ignorePkg, ",") {
if pkg != "" {
checker.Exclusions.Packages = append(checker.Exclusions.Packages, pkg)
}
}
checker.Exclusions.SymbolRegexpsByPackage = ignore
paths := flags.Args()
if len(paths) == 0 {
paths = []string{"."}
}
return paths, exitCodeOk
}
// readExcludes reads an excludes file, a newline delimited file that lists
// patterns for which to allow unchecked errors.
func readExcludes(path string) ([]string, error) {
var excludes []string
buf, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(bytes.NewReader(buf))
for scanner.Scan() {
name := scanner.Text()
// Skip comments and empty lines.
if strings.HasPrefix(name, "//") || name == "" {
continue
}
excludes = append(excludes, name)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return excludes, nil
}
func main() {
os.Exit(mainCmd(os.Args))
}