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 }