github.com/serversong/goreporter@v0.0.0-20200325104552-3cfaf44fd178/linters/errorcheck/errorcheck.go (about)

     1  // Package errcheck is the library used to implement the errcheck command-line tool.
     2  //
     3  // Note: The API of this package has not been finalized and may change at any point.
     4  package errorcheck
     5  
     6  import (
     7  	"bufio"
     8  	"errors"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/build"
    12  	"go/token"
    13  	"go/types"
    14  	"os"
    15  	"path/filepath"
    16  	"regexp"
    17  	"sort"
    18  	"strings"
    19  	"sync"
    20  
    21  	"golang.org/x/tools/go/loader"
    22  )
    23  
    24  var errorType *types.Interface
    25  
    26  func init() {
    27  	errorType = types.Universe.Lookup("error").Type().Underlying().(*types.Interface)
    28  
    29  }
    30  
    31  var (
    32  	// ErrNoGoFiles is returned when CheckPackage is run on a package with no Go source files
    33  	ErrNoGoFiles = errors.New("package contains no go source files")
    34  )
    35  
    36  // UncheckedError indicates the position of an unchecked error return.
    37  type UncheckedError struct {
    38  	Pos      token.Position
    39  	Line     string
    40  	FuncName string
    41  }
    42  
    43  // UncheckedErrors is returned from the CheckPackage function if the package contains
    44  // any unchecked errors.
    45  // Errors should be appended using the Append method, which is safe to use concurrently.
    46  type UncheckedErrors struct {
    47  	mu sync.Mutex
    48  
    49  	// Errors is a list of all the unchecked errors in the package.
    50  	// Printing an error reports its position within the file and the contents of the line.
    51  	Errors []UncheckedError
    52  }
    53  
    54  func (e *UncheckedErrors) Append(errors ...UncheckedError) {
    55  	e.mu.Lock()
    56  	defer e.mu.Unlock()
    57  	e.Errors = append(e.Errors, errors...)
    58  }
    59  
    60  func (e *UncheckedErrors) Error() string {
    61  	return fmt.Sprintf("%d unchecked errors", len(e.Errors))
    62  }
    63  
    64  // Len is the number of elements in the collection.
    65  func (e *UncheckedErrors) Len() int { return len(e.Errors) }
    66  
    67  // Swap swaps the elements with indexes i and j.
    68  func (e *UncheckedErrors) Swap(i, j int) { e.Errors[i], e.Errors[j] = e.Errors[j], e.Errors[i] }
    69  
    70  type byName struct{ *UncheckedErrors }
    71  
    72  // Less reports whether the element with index i should sort before the element with index j.
    73  func (e byName) Less(i, j int) bool {
    74  	ei, ej := e.Errors[i], e.Errors[j]
    75  
    76  	pi, pj := ei.Pos, ej.Pos
    77  
    78  	if pi.Filename != pj.Filename {
    79  		return pi.Filename < pj.Filename
    80  	}
    81  	if pi.Line != pj.Line {
    82  		return pi.Line < pj.Line
    83  	}
    84  	if pi.Column != pj.Column {
    85  		return pi.Column < pj.Column
    86  	}
    87  
    88  	return ei.Line < ej.Line
    89  }
    90  
    91  type Checker struct {
    92  	// ignore is a map of package names to regular expressions. Identifiers from a package are
    93  	// checked against its regular expressions and if any of the expressions match the call
    94  	// is not checked.
    95  	Ignore map[string]*regexp.Regexp
    96  
    97  	// If blank is true then assignments to the blank identifier are also considered to be
    98  	// ignored errors.
    99  	Blank bool
   100  
   101  	// If asserts is true then ignored type assertion results are also checked
   102  	Asserts bool
   103  
   104  	// build tags
   105  	Tags []string
   106  
   107  	Verbose bool
   108  
   109  	// If true, checking of of _test.go files is disabled
   110  	WithoutTests bool
   111  
   112  	exclude map[string]bool
   113  }
   114  
   115  func NewChecker() *Checker {
   116  	c := Checker{}
   117  	c.SetExclude(map[string]bool{})
   118  	return &c
   119  }
   120  
   121  func (c *Checker) SetExclude(l map[string]bool) {
   122  	// Default exclude for stdlib functions
   123  	c.exclude = map[string]bool{
   124  		"math/rand.Read":         true,
   125  		"(*math/rand.Rand).Read": true,
   126  
   127  		"(*bytes.Buffer).Write":       true,
   128  		"(*bytes.Buffer).WriteByte":   true,
   129  		"(*bytes.Buffer).WriteRune":   true,
   130  		"(*bytes.Buffer).WriteString": true,
   131  	}
   132  	for k := range l {
   133  		c.exclude[k] = true
   134  	}
   135  }
   136  
   137  func (c *Checker) logf(msg string, args ...interface{}) {
   138  	if c.Verbose {
   139  		fmt.Fprintf(os.Stderr, msg+"\n", args...)
   140  	}
   141  }
   142  
   143  func (c *Checker) load(paths ...string) (*loader.Program, error) {
   144  	ctx := build.Default
   145  	ctx.BuildTags = append(ctx.BuildTags, c.Tags...)
   146  	loadcfg := loader.Config{
   147  		Build: &ctx,
   148  	}
   149  	rest, err := loadcfg.FromArgs(paths, !c.WithoutTests)
   150  	if err != nil {
   151  		return nil, fmt.Errorf("could not parse arguments: %s", err)
   152  	}
   153  	if len(rest) > 0 {
   154  		return nil, fmt.Errorf("unhandled extra arguments: %v", rest)
   155  	}
   156  
   157  	return loadcfg.Load()
   158  }
   159  
   160  func ErrorCheck(projectPath string) []string {
   161  	errorcheck := NewChecker()
   162  	errorcheck.Asserts = false
   163  	errorcheck.Blank = false
   164  	errorcheck.WithoutTests = true
   165  	errorcheck.Verbose = true
   166  
   167  	uncheckedErrors, err := errorcheck.CheckPackages(projectPath)
   168  	if err != nil {
   169  		return nil
   170  	}
   171  
   172  	return uncheckedErrors
   173  }
   174  
   175  // CheckPackages checks packages for errors.
   176  func (c *Checker) CheckPackages(paths ...string) ([]string, error) {
   177  	program, err := c.load(paths...)
   178  	if err != nil {
   179  		return nil, fmt.Errorf("could not type check: %s", err)
   180  	}
   181  
   182  	var wg sync.WaitGroup
   183  	u := &UncheckedErrors{}
   184  	for _, pkgInfo := range program.InitialPackages() {
   185  		if pkgInfo.Pkg.Path() == "unsafe" { // not a real package
   186  			continue
   187  		}
   188  
   189  		wg.Add(1)
   190  
   191  		go func(pkgInfo *loader.PackageInfo) {
   192  			defer wg.Done()
   193  			c.logf("Checking %s", pkgInfo.Pkg.Path())
   194  
   195  			v := &visitor{
   196  				prog:    program,
   197  				pkg:     pkgInfo,
   198  				ignore:  c.Ignore,
   199  				blank:   c.Blank,
   200  				asserts: c.Asserts,
   201  				lines:   make(map[string][]string),
   202  				exclude: c.exclude,
   203  				errors:  []UncheckedError{},
   204  			}
   205  
   206  			for _, astFile := range v.pkg.Files {
   207  				ast.Walk(v, astFile)
   208  			}
   209  			u.Append(v.errors...)
   210  		}(pkgInfo)
   211  	}
   212  
   213  	wg.Wait()
   214  	if u.Len() > 0 {
   215  		sort.Sort(byName{u})
   216  
   217  		return reportUncheckedErrors(u, c.Verbose), nil
   218  	}
   219  	return nil, nil
   220  }
   221  
   222  // visitor implements the errcheck algorithm
   223  type visitor struct {
   224  	prog    *loader.Program
   225  	pkg     *loader.PackageInfo
   226  	ignore  map[string]*regexp.Regexp
   227  	blank   bool
   228  	asserts bool
   229  	lines   map[string][]string
   230  	exclude map[string]bool
   231  
   232  	errors []UncheckedError
   233  }
   234  
   235  func (v *visitor) fullName(call *ast.CallExpr) (string, bool) {
   236  	sel, ok := call.Fun.(*ast.SelectorExpr)
   237  	if !ok {
   238  		return "", false
   239  	}
   240  	fn, ok := v.pkg.ObjectOf(sel.Sel).(*types.Func)
   241  	if !ok {
   242  		// Shouldn't happen, but be paranoid
   243  		return "", false
   244  	}
   245  	// The name is fully qualified by the import path, possible type,
   246  	// function/method name and pointer receiver.
   247  	//
   248  	// TODO(dh): vendored packages will have /vendor/ in their name,
   249  	// thus not matching vendored standard library packages. If we
   250  	// want to support vendored stdlib packages, we need to implement
   251  	// FullName with our own logic.
   252  	return fn.FullName(), true
   253  }
   254  
   255  func (v *visitor) excludeCall(call *ast.CallExpr) bool {
   256  	if name, ok := v.fullName(call); ok {
   257  		return v.exclude[name]
   258  	}
   259  
   260  	return false
   261  }
   262  
   263  func (v *visitor) ignoreCall(call *ast.CallExpr) bool {
   264  	if v.excludeCall(call) {
   265  		return true
   266  	}
   267  
   268  	// Try to get an identifier.
   269  	// Currently only supports simple expressions:
   270  	//     1. f()
   271  	//     2. x.y.f()
   272  	var id *ast.Ident
   273  	switch exp := call.Fun.(type) {
   274  	case (*ast.Ident):
   275  		id = exp
   276  	case (*ast.SelectorExpr):
   277  		id = exp.Sel
   278  	default:
   279  		// eg: *ast.SliceExpr, *ast.IndexExpr
   280  	}
   281  
   282  	if id == nil {
   283  		return false
   284  	}
   285  
   286  	// If we got an identifier for the function, see if it is ignored
   287  	if re, ok := v.ignore[""]; ok && re.MatchString(id.Name) {
   288  		return true
   289  	}
   290  
   291  	if obj := v.pkg.Uses[id]; obj != nil {
   292  		if pkg := obj.Pkg(); pkg != nil {
   293  			if re, ok := v.ignore[pkg.Path()]; ok {
   294  				return re.MatchString(id.Name)
   295  			}
   296  
   297  			// if current package being considered is vendored, check to see if it should be ignored based
   298  			// on the unvendored path.
   299  			if nonVendoredPkg, ok := nonVendoredPkgPath(pkg.Path()); ok {
   300  				if re, ok := v.ignore[nonVendoredPkg]; ok {
   301  					return re.MatchString(id.Name)
   302  				}
   303  			}
   304  		}
   305  	}
   306  
   307  	return false
   308  }
   309  
   310  // nonVendoredPkgPath returns the unvendored version of the provided package path (or returns the provided path if it
   311  // does not represent a vendored path). The second return value is true if the provided package was vendored, false
   312  // otherwise.
   313  func nonVendoredPkgPath(pkgPath string) (string, bool) {
   314  	lastVendorIndex := strings.LastIndex(pkgPath, "/vendor/")
   315  	if lastVendorIndex == -1 {
   316  		return pkgPath, false
   317  	}
   318  	return pkgPath[lastVendorIndex+len("/vendor/"):], true
   319  }
   320  
   321  // errorsByArg returns a slice s such that
   322  // len(s) == number of return types of call
   323  // s[i] == true iff return type at position i from left is an error type
   324  func (v *visitor) errorsByArg(call *ast.CallExpr) []bool {
   325  	switch t := v.pkg.Types[call].Type.(type) {
   326  	case *types.Named:
   327  		// Single return
   328  		return []bool{isErrorType(t)}
   329  	case *types.Pointer:
   330  		// Single return via pointer
   331  		return []bool{isErrorType(t)}
   332  	case *types.Tuple:
   333  		// Multiple returns
   334  		s := make([]bool, t.Len())
   335  		for i := 0; i < t.Len(); i++ {
   336  			switch et := t.At(i).Type().(type) {
   337  			case *types.Named:
   338  				// Single return
   339  				s[i] = isErrorType(et)
   340  			case *types.Pointer:
   341  				// Single return via pointer
   342  				s[i] = isErrorType(et)
   343  			default:
   344  				s[i] = false
   345  			}
   346  		}
   347  		return s
   348  	}
   349  	return []bool{false}
   350  }
   351  
   352  func (v *visitor) callReturnsError(call *ast.CallExpr) bool {
   353  	if v.isRecover(call) {
   354  		return true
   355  	}
   356  	for _, isError := range v.errorsByArg(call) {
   357  		if isError {
   358  			return true
   359  		}
   360  	}
   361  	return false
   362  }
   363  
   364  // isRecover returns true if the given CallExpr is a call to the built-in recover() function.
   365  func (v *visitor) isRecover(call *ast.CallExpr) bool {
   366  	if fun, ok := call.Fun.(*ast.Ident); ok {
   367  		if _, ok := v.pkg.Uses[fun].(*types.Builtin); ok {
   368  			return fun.Name == "recover"
   369  		}
   370  	}
   371  	return false
   372  }
   373  
   374  func (v *visitor) addErrorAtPosition(position token.Pos, call *ast.CallExpr) {
   375  	pos := v.prog.Fset.Position(position)
   376  	lines, ok := v.lines[pos.Filename]
   377  	if !ok {
   378  		lines = readfile(pos.Filename)
   379  		v.lines[pos.Filename] = lines
   380  	}
   381  
   382  	line := "??"
   383  	if pos.Line-1 < len(lines) {
   384  		line = strings.TrimSpace(lines[pos.Line-1])
   385  	}
   386  
   387  	var name string
   388  	if call != nil {
   389  		name, _ = v.fullName(call)
   390  	}
   391  
   392  	v.errors = append(v.errors, UncheckedError{pos, line, name})
   393  }
   394  
   395  func readfile(filename string) []string {
   396  	var f, err = os.Open(filename)
   397  	if err != nil {
   398  		return nil
   399  	}
   400  
   401  	var lines []string
   402  	var scanner = bufio.NewScanner(f)
   403  	for scanner.Scan() {
   404  		lines = append(lines, scanner.Text())
   405  	}
   406  	return lines
   407  }
   408  
   409  func (v *visitor) Visit(node ast.Node) ast.Visitor {
   410  	switch stmt := node.(type) {
   411  	case *ast.ExprStmt:
   412  		if call, ok := stmt.X.(*ast.CallExpr); ok {
   413  			if !v.ignoreCall(call) && v.callReturnsError(call) {
   414  				v.addErrorAtPosition(call.Lparen, call)
   415  			}
   416  		}
   417  	case *ast.GoStmt:
   418  		if !v.ignoreCall(stmt.Call) && v.callReturnsError(stmt.Call) {
   419  			v.addErrorAtPosition(stmt.Call.Lparen, stmt.Call)
   420  		}
   421  	case *ast.DeferStmt:
   422  		if !v.ignoreCall(stmt.Call) && v.callReturnsError(stmt.Call) {
   423  			v.addErrorAtPosition(stmt.Call.Lparen, stmt.Call)
   424  		}
   425  	case *ast.AssignStmt:
   426  		if len(stmt.Rhs) == 1 {
   427  			// single value on rhs; check against lhs identifiers
   428  			if call, ok := stmt.Rhs[0].(*ast.CallExpr); ok {
   429  				if !v.blank {
   430  					break
   431  				}
   432  				if v.ignoreCall(call) {
   433  					break
   434  				}
   435  				isError := v.errorsByArg(call)
   436  				for i := 0; i < len(stmt.Lhs); i++ {
   437  					if id, ok := stmt.Lhs[i].(*ast.Ident); ok {
   438  						// We shortcut calls to recover() because errorsByArg can't
   439  						// check its return types for errors since it returns interface{}.
   440  						if id.Name == "_" && (v.isRecover(call) || isError[i]) {
   441  							v.addErrorAtPosition(id.NamePos, call)
   442  						}
   443  					}
   444  				}
   445  			} else if assert, ok := stmt.Rhs[0].(*ast.TypeAssertExpr); ok {
   446  				if !v.asserts {
   447  					break
   448  				}
   449  				if assert.Type == nil {
   450  					// type switch
   451  					break
   452  				}
   453  				if len(stmt.Lhs) < 2 {
   454  					// assertion result not read
   455  					v.addErrorAtPosition(stmt.Rhs[0].Pos(), nil)
   456  				} else if id, ok := stmt.Lhs[1].(*ast.Ident); ok && v.blank && id.Name == "_" {
   457  					// assertion result ignored
   458  					v.addErrorAtPosition(id.NamePos, nil)
   459  				}
   460  			}
   461  		} else {
   462  			// multiple value on rhs; in this case a call can't return
   463  			// multiple values. Assume len(stmt.Lhs) == len(stmt.Rhs)
   464  			for i := 0; i < len(stmt.Lhs); i++ {
   465  				if id, ok := stmt.Lhs[i].(*ast.Ident); ok {
   466  					if call, ok := stmt.Rhs[i].(*ast.CallExpr); ok {
   467  						if !v.blank {
   468  							continue
   469  						}
   470  						if v.ignoreCall(call) {
   471  							continue
   472  						}
   473  						if id.Name == "_" && v.callReturnsError(call) {
   474  							v.addErrorAtPosition(id.NamePos, call)
   475  						}
   476  					} else if assert, ok := stmt.Rhs[i].(*ast.TypeAssertExpr); ok {
   477  						if !v.asserts {
   478  							continue
   479  						}
   480  						if assert.Type == nil {
   481  							// Shouldn't happen anyway, no multi assignment in type switches
   482  							continue
   483  						}
   484  						v.addErrorAtPosition(id.NamePos, nil)
   485  					}
   486  				}
   487  			}
   488  		}
   489  	default:
   490  	}
   491  	return v
   492  }
   493  
   494  func isErrorType(t types.Type) bool {
   495  	return types.Implements(t, errorType)
   496  }
   497  
   498  func reportUncheckedErrors(e *UncheckedErrors, verbose bool) []string {
   499  	uncheckedErrorArray := make([]string, 0)
   500  	wd, err := os.Getwd()
   501  	if err != nil {
   502  		wd = ""
   503  	}
   504  	for _, uncheckedError := range e.Errors {
   505  		pos := uncheckedError.Pos.String()
   506  
   507  		newPos, err := filepath.Rel(wd, pos)
   508  		if err == nil {
   509  			pos = newPos
   510  		}
   511  
   512  		if verbose && uncheckedError.FuncName != "" {
   513  			uncheckedErrorArray = append(uncheckedErrorArray, fmt.Sprintf("%s:\t%s\t%s\n", pos, uncheckedError.FuncName, uncheckedError.Line))
   514  		} else {
   515  			uncheckedErrorArray = append(uncheckedErrorArray, fmt.Sprintf("%s:\t%s\n", pos, uncheckedError.Line))
   516  		}
   517  	}
   518  	return uncheckedErrorArray
   519  }