go/loader: make (*Config).Load() robust against I/O, scanner and parser errors.
Before, Load() would just fail. Now, it gathers all frontend
errors (not just the first go/types error) in PackageInfo.Errors.
There are still cases where Load() can fail hard, e.g. errors in x_test.go
files. That case is trickier to fix and remains a TODO item.
Also, make godoc display all scanner/parser/type errors in the source view.
LGTM=gri
R=gri
CC=golang-codereviews
https://golang.org/cl/108940043
diff --git a/go/loader/loader.go b/go/loader/loader.go
index 534cfd1..2a15359 100644
--- a/go/loader/loader.go
+++ b/go/loader/loader.go
@@ -121,6 +121,10 @@
// - cache the calls to build.Import so we don't do it three times per
// test package.
// - Thorough overhaul of package documentation.
+// - Certain errors (e.g. parse error in x_test.go files, or failure to
+// import an initial package) still cause Load() to fail hard.
+// Fix that. (It's tricky because of the way x_test files are parsed
+// eagerly.)
import (
"errors"
@@ -199,11 +203,11 @@
// leaking into the user interface.
DisplayPath func(path string) string
- // If AllowTypeErrors is true, Load will return a Program even
- // if some of the its packages contained type errors; such
- // errors are accessible via PackageInfo.TypeError.
- // If false, Load will fail if any package had a type error.
- AllowTypeErrors bool
+ // If AllowErrors is true, Load will return a Program even
+ // if some of the its packages contained I/O, parser or type
+ // errors; such errors are accessible via PackageInfo.Errors. If
+ // false, Load will fail if any package had an error.
+ AllowErrors bool
// CreatePkgs specifies a list of non-importable initial
// packages to create. Each element specifies a list of
@@ -348,12 +352,13 @@
// specified *.go files and adds a package entry for them to
// conf.CreatePkgs.
//
+// It fails if any file could not be loaded or parsed.
+//
func (conf *Config) CreateFromFilenames(path string, filenames ...string) error {
- files, err := parseFiles(conf.fset(), conf.build(), nil, ".", filenames, conf.ParserMode)
- if err != nil {
- return err
+ files, errs := parseFiles(conf.fset(), conf.build(), nil, ".", filenames, conf.ParserMode)
+ if len(errs) > 0 {
+ return errs[0]
}
-
conf.CreateFromFiles(path, files...)
return nil
}
@@ -388,9 +393,15 @@
conf.Import(path)
// Load the external test package.
- xtestFiles, err := conf.parsePackageFiles(path, 'x')
+ bp, err := conf.findSourcePackage(path)
if err != nil {
- return err
+ return err // package not found
+ }
+ xtestFiles, errs := conf.parsePackageFiles(bp, 'x')
+ if len(errs) > 0 {
+ // TODO(adonovan): fix: parse errors in x_test.go files
+ // are still catastrophic to Load().
+ return errs[0] // I/O or parse error
}
if len(xtestFiles) > 0 {
conf.CreateFromFiles(path+"_test", xtestFiles...)
@@ -460,7 +471,7 @@
// importInfo tracks the success or failure of a single import.
type importInfo struct {
- info *PackageInfo // results of typechecking (including type errors)
+ info *PackageInfo // results of typechecking (including errors)
err error // reason for failure to make a package
}
@@ -470,8 +481,10 @@
// On success, Load returns a Program containing a PackageInfo for
// each package. On failure, it returns an error.
//
-// If conf.AllowTypeErrors is set, a type error does not cause Load to
-// fail, but is recorded in the PackageInfo.TypeError field.
+// If AllowErrors is true, Load will return a Program even if some
+// packages contained I/O, parser or type errors, or if dependencies
+// were missing. (Such errors are accessible via PackageInfo.Errors. If
+// false, Load will fail if any package had an error.
//
// It is an error if no packages were loaded.
//
@@ -498,9 +511,7 @@
for path := range conf.ImportPkgs {
info, err := imp.importPackage(path)
if err != nil {
- // TODO(adonovan): don't abort Load just
- // because of a parse error in one package.
- return nil, err // e.g. parse error (but not type error)
+ return nil, err // failed to create package
}
prog.Imported[path] = info
}
@@ -508,15 +519,17 @@
// Now augment those packages that need it.
for path, augment := range conf.ImportPkgs {
if augment {
- ii := imp.imported[path]
-
// Find and create the actual package.
- files, err := imp.conf.parsePackageFiles(path, 't')
- // Prefer the earlier error, if any.
- if err != nil && ii.err == nil {
- ii.err = err // e.g. parse error.
+ bp, err := conf.findSourcePackage(path)
+ if err != nil {
+ // "Can't happen" because of previous loop.
+ return nil, err // package not found
}
- typeCheckFiles(ii.info, files...)
+
+ info := imp.imported[path].info // must be non-nil, see above
+ files, errs := imp.conf.parsePackageFiles(bp, 't')
+ info.Errors = append(info.Errors, errs...)
+ typeCheckFiles(info, files...)
}
}
@@ -545,16 +558,16 @@
}
}
- if !conf.AllowTypeErrors {
+ if !conf.AllowErrors {
// Report errors in indirectly imported packages.
var errpkgs []string
for _, info := range prog.AllPackages {
- if info.TypeError != nil {
+ if len(info.Errors) > 0 {
errpkgs = append(errpkgs, info.Pkg.Path())
}
}
if errpkgs != nil {
- return nil, fmt.Errorf("couldn't load packages due to type errors: %s",
+ return nil, fmt.Errorf("couldn't load packages due to errors: %s",
strings.Join(errpkgs, ", "))
}
}
@@ -592,7 +605,7 @@
}
}
for _, info := range allPackages {
- if info.TypeError != nil {
+ if len(info.Errors) > 0 {
visit(info.Pkg)
}
}
@@ -613,26 +626,28 @@
return &build.Default
}
+// findSourcePackage locates the specified (possibly empty) package
+// using go/build logic. It returns an error if not found.
+//
+func (conf *Config) findSourcePackage(path string) (*build.Package, error) {
+ // Import(srcDir="") disables local imports, e.g. import "./foo".
+ bp, err := conf.build().Import(path, "", 0)
+ if _, ok := err.(*build.NoGoError); ok {
+ return bp, nil // empty directory is not an error
+ }
+ return bp, err
+}
+
// parsePackageFiles enumerates the files belonging to package path,
-// then loads, parses and returns them.
+// then loads, parses and returns them, plus a list of I/O or parse
+// errors that were encountered.
//
// 'which' indicates which files to include:
// 'g': include non-test *.go source files (GoFiles + processed CgoFiles)
// 't': include in-package *_test.go source files (TestGoFiles)
// 'x': include external *_test.go source files. (XTestGoFiles)
//
-// TODO(adonovan): don't fail just because some files contain parse errors.
-//
-func (conf *Config) parsePackageFiles(path string, which rune) ([]*ast.File, error) {
- // Import(srcDir="") disables local imports, e.g. import "./foo".
- bp, err := conf.build().Import(path, "", 0)
- if _, ok := err.(*build.NoGoError); ok {
- return nil, nil // empty directory
- }
- if err != nil {
- return nil, err // import failed
- }
-
+func (conf *Config) parsePackageFiles(bp *build.Package, which rune) ([]*ast.File, []error) {
var filenames []string
switch which {
case 'g':
@@ -645,21 +660,19 @@
panic(which)
}
- files, err := parseFiles(conf.fset(), conf.build(), conf.DisplayPath, bp.Dir, filenames, conf.ParserMode)
- if err != nil {
- return nil, err
- }
+ files, errs := parseFiles(conf.fset(), conf.build(), conf.DisplayPath, bp.Dir, filenames, conf.ParserMode)
// Preprocess CgoFiles and parse the outputs (sequentially).
if which == 'g' && bp.CgoFiles != nil {
cgofiles, err := processCgoFiles(bp, conf.fset(), conf.ParserMode)
if err != nil {
- return nil, err
+ errs = append(errs, err)
+ } else {
+ files = append(files, cgofiles...)
}
- files = append(files, cgofiles...)
}
- return files, nil
+ return files, errs
}
// doImport imports the package denoted by path.
@@ -694,6 +707,9 @@
// importPackage imports the package with the given import path, plus
// its dependencies.
//
+// On success, it returns a PackageInfo, possibly containing errors.
+// importPackage returns an error if it couldn't even create the package.
+//
// Precondition: path != "unsafe".
//
func (imp *importer) importPackage(path string) (*PackageInfo, error) {
@@ -741,12 +757,14 @@
// located by go/build.
//
func (imp *importer) importFromSource(path string) (*PackageInfo, error) {
- files, err := imp.conf.parsePackageFiles(path, 'g')
+ bp, err := imp.conf.findSourcePackage(path)
if err != nil {
- return nil, err
+ return nil, err // package not found
}
// Type-check the package.
info := imp.newPackageInfo(path)
+ files, errs := imp.conf.parsePackageFiles(bp, 'g')
+ info.Errors = append(info.Errors, errs...)
typeCheckFiles(info, files...)
return info, nil
}
@@ -755,29 +773,15 @@
// The order of files determines the package initialization order.
// It may be called multiple times.
//
-// Any error is stored in the info.TypeError field.
+// Any error is stored in the info.Error field.
func typeCheckFiles(info *PackageInfo, files ...*ast.File) {
info.Files = append(info.Files, files...)
- if err := info.checker.Files(files); err != nil {
- // Prefer the earlier error, if any.
- if info.TypeError == nil {
- info.TypeError = err
- }
- }
+
+ // Ignore the returned (first) error since we already collect them all.
+ _ = info.checker.Files(files)
}
func (imp *importer) newPackageInfo(path string) *PackageInfo {
- // Use a copy of the types.Config so we can vary IgnoreFuncBodies.
- tc := imp.conf.TypeChecker
- tc.IgnoreFuncBodies = false
- if f := imp.conf.TypeCheckFuncBodies; f != nil {
- tc.IgnoreFuncBodies = !f(path)
- }
- if tc.Error == nil {
- tc.Error = func(e error) { fmt.Fprintln(os.Stderr, e) }
- }
- tc.Import = imp.doImport // doImport wraps the user's importfn, effectively
-
pkg := types.NewPackage(path, "")
info := &PackageInfo{
Pkg: pkg,
@@ -790,6 +794,26 @@
Selections: make(map[*ast.SelectorExpr]*types.Selection),
},
}
+
+ // Use a copy of the types.Config so we can vary IgnoreFuncBodies.
+ tc := imp.conf.TypeChecker
+ tc.IgnoreFuncBodies = false
+ if f := imp.conf.TypeCheckFuncBodies; f != nil {
+ tc.IgnoreFuncBodies = !f(path)
+ }
+ tc.Import = imp.doImport // doImport wraps the user's importfn, effectively
+
+ // The default error reporter just prints the errors.
+ dfltError := tc.Error
+ if dfltError == nil {
+ dfltError = func(e error) { fmt.Fprintln(os.Stderr, e) }
+ }
+ // Wrap the error reporter to also collect them.
+ tc.Error = func(e error) {
+ info.Errors = append(info.Errors, e)
+ dfltError(e)
+ }
+
info.checker = types.NewChecker(&tc, imp.conf.fset(), pkg, &info.Info)
imp.prog.AllPackages[pkg] = info
return info