88 "errors"
99 "fmt"
1010 "go/ast"
11- "go/build"
1211 "go/token"
1312 "go/types"
1413 "os"
@@ -17,8 +16,7 @@ import (
1716 "strings"
1817 "sync"
1918
20- "go/parser"
21- "golang.org/x/tools/go/loader"
19+ "golang.org/x/tools/go/packages"
2220)
2321
2422var errorType * types.Interface
@@ -137,6 +135,15 @@ func (c *Checker) SetExclude(l map[string]bool) {
137135 "fmt.Print" ,
138136 "fmt.Printf" ,
139137 "fmt.Println" ,
138+ "fmt.Fprint(*bytes.Buffer)" ,
139+ "fmt.Fprintf(*bytes.Buffer)" ,
140+ "fmt.Fprintln(*bytes.Buffer)" ,
141+ "fmt.Fprint(*strings.Builder)" ,
142+ "fmt.Fprintf(*strings.Builder)" ,
143+ "fmt.Fprintln(*strings.Builder)" ,
144+ "fmt.Fprint(os.Stderr)" ,
145+ "fmt.Fprintf(os.Stderr)" ,
146+ "fmt.Fprintln(os.Stderr)" ,
140147
141148 // math/rand
142149 "math/rand.Read" ,
@@ -165,28 +172,18 @@ func (c *Checker) logf(msg string, args ...interface{}) {
165172 }
166173}
167174
168- func (c * Checker ) load (paths ... string ) (* loader.Program , error ) {
169- ctx := build .Default
170- for _ , tag := range c .Tags {
171- ctx .BuildTags = append (ctx .BuildTags , tag )
172- }
173- loadcfg := loader.Config {
174- Build : & ctx ,
175- }
176-
177- if c .WithoutGeneratedCode {
178- loadcfg .ParserMode = parser .ParseComments
179- }
175+ // loadPackages is used for testing.
176+ var loadPackages = func (cfg * packages.Config , paths ... string ) ([]* packages.Package , error ) {
177+ return packages .Load (cfg , paths ... )
178+ }
180179
181- rest , err := loadcfg .FromArgs (paths , ! c .WithoutTests )
182- if err != nil {
183- return nil , fmt .Errorf ("could not parse arguments: %s" , err )
184- }
185- if len (rest ) > 0 {
186- return nil , fmt .Errorf ("unhandled extra arguments: %v" , rest )
180+ func (c * Checker ) load (paths ... string ) ([]* packages.Package , error ) {
181+ cfg := & packages.Config {
182+ Mode : packages .LoadAllSyntax ,
183+ Tests : ! c .WithoutTests ,
184+ BuildFlags : []string {fmt .Sprintf ("-tags=%s" , strings .Join (c .Tags , " " ))},
187185 }
188-
189- return loadcfg .Load ()
186+ return loadPackages (cfg , paths ... )
190187}
191188
192189var generatedCodeRegexp = regexp .MustCompile ("^// Code generated .* DO NOT EDIT\\ .$" )
@@ -209,27 +206,28 @@ func (c *Checker) shouldSkipFile(file *ast.File) bool {
209206
210207// CheckPackages checks packages for errors.
211208func (c * Checker ) CheckPackages (paths ... string ) error {
212- program , err := c .load (paths ... )
209+ pkgs , err := c .load (paths ... )
213210 if err != nil {
214- return fmt .Errorf ("could not type check: %s" , err )
211+ return err
212+ }
213+ // Check for errors in the initial packages.
214+ for _ , pkg := range pkgs {
215+ if len (pkg .Errors ) > 0 {
216+ return fmt .Errorf ("errors while loading package %s: %v" , pkg .ID , pkg .Errors )
217+ }
215218 }
216219
217220 var wg sync.WaitGroup
218221 u := & UncheckedErrors {}
219- for _ , pkgInfo := range program .InitialPackages () {
220- if pkgInfo .Pkg .Path () == "unsafe" { // not a real package
221- continue
222- }
223-
222+ for _ , pkg := range pkgs {
224223 wg .Add (1 )
225224
226- go func (pkgInfo * loader. PackageInfo ) {
225+ go func (pkg * packages. Package ) {
227226 defer wg .Done ()
228- c .logf ("Checking %s" , pkgInfo . Pkg .Path ())
227+ c .logf ("Checking %s" , pkg . Types .Path ())
229228
230229 v := & visitor {
231- prog : program ,
232- pkg : pkgInfo ,
230+ pkg : pkg ,
233231 ignore : c .Ignore ,
234232 blank : c .Blank ,
235233 asserts : c .Asserts ,
@@ -238,28 +236,36 @@ func (c *Checker) CheckPackages(paths ...string) error {
238236 errors : []UncheckedError {},
239237 }
240238
241- for _ , astFile := range v .pkg .Files {
239+ for _ , astFile := range v .pkg .Syntax {
242240 if c .shouldSkipFile (astFile ) {
243241 continue
244242 }
245243 ast .Walk (v , astFile )
246244 }
247245 u .Append (v .errors ... )
248- }(pkgInfo )
246+ }(pkg )
249247 }
250248
251249 wg .Wait ()
252250 if u .Len () > 0 {
251+ // Sort unchecked errors and remove duplicates. Duplicates may occur when a file
252+ // containing an unchecked error belongs to > 1 package.
253253 sort .Sort (byName {u })
254+ uniq := u .Errors [:0 ] // compact in-place
255+ for i , err := range u .Errors {
256+ if i == 0 || err != u .Errors [i - 1 ] {
257+ uniq = append (uniq , err )
258+ }
259+ }
260+ u .Errors = uniq
254261 return u
255262 }
256263 return nil
257264}
258265
259266// visitor implements the errcheck algorithm
260267type visitor struct {
261- prog * loader.Program
262- pkg * loader.PackageInfo
268+ pkg * packages.Package
263269 ignore map [string ]* regexp.Regexp
264270 blank bool
265271 asserts bool
@@ -284,7 +290,7 @@ func (v *visitor) selectorAndFunc(call *ast.CallExpr) (*ast.SelectorExpr, *types
284290 return nil , nil , false
285291 }
286292
287- fn , ok := v .pkg .ObjectOf (sel .Sel ).(* types.Func )
293+ fn , ok := v .pkg .TypesInfo . ObjectOf (sel .Sel ).(* types.Func )
288294 if ! ok {
289295 // Shouldn't happen, but be paranoid
290296 return nil , nil , false
@@ -340,7 +346,7 @@ func (v *visitor) namesForExcludeCheck(call *ast.CallExpr) []string {
340346
341347 // This will be missing for functions without a receiver (like fmt.Printf),
342348 // so just fall back to the the function's fullName in that case.
343- selection , ok := v .pkg .Selections [sel ]
349+ selection , ok := v .pkg .TypesInfo . Selections [sel ]
344350 if ! ok {
345351 return []string {name }
346352 }
@@ -363,13 +369,37 @@ func (v *visitor) namesForExcludeCheck(call *ast.CallExpr) []string {
363369 return result
364370}
365371
372+ // isBufferType checks if the expression type is a known in-memory buffer type.
373+ func (v * visitor ) argName (expr ast.Expr ) string {
374+ // Special-case literal "os.Stdout" and "os.Stderr"
375+ if sel , ok := expr .(* ast.SelectorExpr ); ok {
376+ if obj := v .pkg .TypesInfo .ObjectOf (sel .Sel ); obj != nil {
377+ vr , ok := obj .(* types.Var )
378+ if ok && vr .Pkg () != nil && vr .Pkg ().Name () == "os" && (vr .Name () == "Stderr" || vr .Name () == "Stdout" ) {
379+ return "os." + vr .Name ()
380+ }
381+ }
382+ }
383+ t := v .pkg .TypesInfo .TypeOf (expr )
384+ if t == nil {
385+ return ""
386+ }
387+ return t .String ()
388+ }
389+
366390func (v * visitor ) excludeCall (call * ast.CallExpr ) bool {
391+ var arg0 string
392+ if len (call .Args ) > 0 {
393+ arg0 = v .argName (call .Args [0 ])
394+ }
367395 for _ , name := range v .namesForExcludeCheck (call ) {
368396 if v .exclude [name ] {
369397 return true
370398 }
399+ if arg0 != "" && v .exclude [name + "(" + arg0 + ")" ] {
400+ return true
401+ }
371402 }
372-
373403 return false
374404}
375405
@@ -401,7 +431,7 @@ func (v *visitor) ignoreCall(call *ast.CallExpr) bool {
401431 return true
402432 }
403433
404- if obj := v .pkg .Uses [id ]; obj != nil {
434+ if obj := v .pkg .TypesInfo . Uses [id ]; obj != nil {
405435 if pkg := obj .Pkg (); pkg != nil {
406436 if re , ok := v .ignore [pkg .Path ()]; ok {
407437 return re .MatchString (id .Name )
@@ -435,7 +465,7 @@ func nonVendoredPkgPath(pkgPath string) (string, bool) {
435465// len(s) == number of return types of call
436466// s[i] == true iff return type at position i from left is an error type
437467func (v * visitor ) errorsByArg (call * ast.CallExpr ) []bool {
438- switch t := v .pkg .Types [call ].Type .(type ) {
468+ switch t := v .pkg .TypesInfo . Types [call ].Type .(type ) {
439469 case * types.Named :
440470 // Single return
441471 return []bool {isErrorType (t )}
@@ -477,15 +507,15 @@ func (v *visitor) callReturnsError(call *ast.CallExpr) bool {
477507// isRecover returns true if the given CallExpr is a call to the built-in recover() function.
478508func (v * visitor ) isRecover (call * ast.CallExpr ) bool {
479509 if fun , ok := call .Fun .(* ast.Ident ); ok {
480- if _ , ok := v .pkg .Uses [fun ].(* types.Builtin ); ok {
510+ if _ , ok := v .pkg .TypesInfo . Uses [fun ].(* types.Builtin ); ok {
481511 return fun .Name == "recover"
482512 }
483513 }
484514 return false
485515}
486516
487517func (v * visitor ) addErrorAtPosition (position token.Pos , call * ast.CallExpr ) {
488- pos := v .prog .Fset .Position (position )
518+ pos := v .pkg .Fset .Position (position )
489519 lines , ok := v .lines [pos .Filename ]
490520 if ! ok {
491521 lines = readfile (pos .Filename )
0 commit comments