Skip to content

Commit

Permalink
cmd/go: eliminate redundancy in import error messages
Browse files Browse the repository at this point in the history
This change introduces a new interface, load.ImportPathError. An error
may satisfy this by providing an ImportPath method and including the
import path in its error text. modload.ImportMissingError satisfies
this interface. load.ImportErrorf also provides a convenient way to
create an error satisfying this interface with an arbitrary message.

When load.PackageError formats its error text, it may omit the last
path on the import stack if the wrapped error satisfies
ImportPathError and has a matching path.

To make this work, PackageError.Err is now an error instead of a
string. PackageError.MarshalJSON will write Err as a string for
'go list -json' output.

When go/build.Import invokes 'go list' in module mode, it now runs
with '-e' and includes '.Error' in the output format instead of
expecting the error to be in the raw stderr text. If a package error
is printed and a directory was not found, the error will be returned
without extra decoration.

Fixes #34752

Change-Id: I2d81dab7dec19e0ae9f51f6412bc9f30433a8596
Reviewed-on: https://go-review.googlesource.com/c/go/+/199840
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
  • Loading branch information
Jay Conrod committed Oct 9, 2019
1 parent b3104fe commit 32b6eb8
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 55 deletions.
2 changes: 1 addition & 1 deletion src/cmd/go/internal/fmtcmd/fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func runFmt(cmd *base.Command, args []string) {
continue
}
if pkg.Error != nil {
if strings.HasPrefix(pkg.Error.Err, "build constraints exclude all Go files") {
if strings.HasPrefix(pkg.Error.Err.Error(), "build constraints exclude all Go files") {
// Skip this error, as we will format
// all files regardless.
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/cmd/go/internal/get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ func download(arg string, parent *load.Package, stk *load.ImportStack, mode int)
stk.Push(arg)
err := downloadPackage(p)
if err != nil {
base.Errorf("%s", &load.PackageError{ImportStack: stk.Copy(), Err: err.Error()})
base.Errorf("%s", &load.PackageError{ImportStack: stk.Copy(), Err: err})
stk.Pop()
return
}
Expand Down Expand Up @@ -355,7 +355,7 @@ func download(arg string, parent *load.Package, stk *load.ImportStack, mode int)
stk.Push(path)
err := &load.PackageError{
ImportStack: stk.Copy(),
Err: "must be imported as " + path[j+len("vendor/"):],
Err: load.ImportErrorf(path, "%s must be imported as %s", path, path[j+len("vendor/"):]),
}
stk.Pop()
base.Errorf("%s", err)
Expand Down
130 changes: 99 additions & 31 deletions src/cmd/go/internal/load/pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package load

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"go/build"
Expand Down Expand Up @@ -304,9 +305,9 @@ func (p *Package) copyBuild(pp *build.Package) {
type PackageError struct {
ImportStack []string // shortest path from package named on command line to this one
Pos string // position of error
Err string // the error itself
IsImportCycle bool `json:"-"` // the error is an import cycle
Hard bool `json:"-"` // whether the error is soft or hard; soft errors are ignored in some places
Err error // the error itself
IsImportCycle bool // the error is an import cycle
Hard bool // whether the error is soft or hard; soft errors are ignored in some places
}

func (p *PackageError) Error() string {
Expand All @@ -317,12 +318,77 @@ func (p *PackageError) Error() string {
if p.Pos != "" {
// Omit import stack. The full path to the file where the error
// is the most important thing.
return p.Pos + ": " + p.Err
return p.Pos + ": " + p.Err.Error()
}
if len(p.ImportStack) == 0 {
return p.Err

// If the error is an ImportPathError, and the last path on the stack appears
// in the error message, omit that path from the stack to avoid repetition.
// If an ImportPathError wraps another ImportPathError that matches the
// last path on the stack, we don't omit the path. An error like
// "package A imports B: error loading C caused by B" would not be clearer
// if "imports B" were omitted.
stack := p.ImportStack
var ierr ImportPathError
if len(stack) > 0 && errors.As(p.Err, &ierr) && ierr.ImportPath() == stack[len(stack)-1] {
stack = stack[:len(stack)-1]
}
if len(stack) == 0 {
return p.Err.Error()
}
return "package " + strings.Join(stack, "\n\timports ") + ": " + p.Err.Error()
}

// PackageError implements MarshalJSON so that Err is marshaled as a string
// and non-essential fields are omitted.
func (p *PackageError) MarshalJSON() ([]byte, error) {
perr := struct {
ImportStack []string
Pos string
Err string
}{p.ImportStack, p.Pos, p.Err.Error()}
return json.Marshal(perr)
}

// ImportPathError is a type of error that prevents a package from being loaded
// for a given import path. When such a package is loaded, a *Package is
// returned with Err wrapping an ImportPathError: the error is attached to
// the imported package, not the importing package.
//
// The string returned by ImportPath must appear in the string returned by
// Error. Errors that wrap ImportPathError (such as PackageError) may omit
// the import path.
type ImportPathError interface {
error
ImportPath() string
}

type importError struct {
importPath string
err error // created with fmt.Errorf
}

var _ ImportPathError = (*importError)(nil)

func ImportErrorf(path, format string, args ...interface{}) ImportPathError {
err := &importError{importPath: path, err: fmt.Errorf(format, args...)}
if errStr := err.Error(); !strings.Contains(errStr, path) {
panic(fmt.Sprintf("path %q not in error %q", path, errStr))
}
return "package " + strings.Join(p.ImportStack, "\n\timports ") + ": " + p.Err
return err
}

func (e *importError) Error() string {
return e.err.Error()
}

func (e *importError) Unwrap() error {
// Don't return e.err directly, since we're only wrapping an error if %w
// was passed to ImportErrorf.
return errors.Unwrap(e.err)
}

func (e *importError) ImportPath() string {
return e.importPath
}

// An ImportStack is a stack of import paths, possibly with the suffix " (test)" appended.
Expand Down Expand Up @@ -489,7 +555,7 @@ func loadImport(pre *preload, path, srcDir string, parent *Package, stk *ImportS
ImportPath: path,
Error: &PackageError{
ImportStack: stk.Copy(),
Err: err.Error(),
Err: err,
},
},
}
Expand All @@ -516,7 +582,7 @@ func loadImport(pre *preload, path, srcDir string, parent *Package, stk *ImportS
if !cfg.ModulesEnabled && path != cleanImport(path) {
p.Error = &PackageError{
ImportStack: stk.Copy(),
Err: fmt.Sprintf("non-canonical import path: %q should be %q", path, pathpkg.Clean(path)),
Err: fmt.Errorf("non-canonical import path: %q should be %q", path, pathpkg.Clean(path)),
}
p.Incomplete = true
}
Expand All @@ -536,20 +602,22 @@ func loadImport(pre *preload, path, srcDir string, parent *Package, stk *ImportS
perr := *p
perr.Error = &PackageError{
ImportStack: stk.Copy(),
Err: fmt.Sprintf("import %q is a program, not an importable package", path),
Err: ImportErrorf(path, "import %q is a program, not an importable package", path),
}
return setErrorPos(&perr, importPos)
}

if p.Internal.Local && parent != nil && !parent.Internal.Local {
perr := *p
errMsg := fmt.Sprintf("local import %q in non-local package", path)
var err error
if path == "." {
errMsg = "cannot import current directory"
err = ImportErrorf(path, "%s: cannot import current directory", path)
} else {
err = ImportErrorf(path, "local import %q in non-local package", path)
}
perr.Error = &PackageError{
ImportStack: stk.Copy(),
Err: errMsg,
Err: err,
}
return setErrorPos(&perr, importPos)
}
Expand Down Expand Up @@ -1125,7 +1193,7 @@ func reusePackage(p *Package, stk *ImportStack) *Package {
if p.Error == nil {
p.Error = &PackageError{
ImportStack: stk.Copy(),
Err: "import cycle not allowed",
Err: errors.New("import cycle not allowed"),
IsImportCycle: true,
}
}
Expand Down Expand Up @@ -1228,7 +1296,7 @@ func disallowInternal(srcDir string, importer *Package, importerPath string, p *
perr := *p
perr.Error = &PackageError{
ImportStack: stk.Copy(),
Err: "use of internal package " + p.ImportPath + " not allowed",
Err: ImportErrorf(p.ImportPath, "use of internal package "+p.ImportPath+" not allowed"),
}
perr.Incomplete = true
return &perr
Expand Down Expand Up @@ -1275,7 +1343,7 @@ func disallowVendor(srcDir string, importer *Package, importerPath, path string,
perr := *p
perr.Error = &PackageError{
ImportStack: stk.Copy(),
Err: "must be imported as " + path[i+len("vendor/"):],
Err: ImportErrorf(path, "%s must be imported as %s", path, path[i+len("vendor/"):]),
}
perr.Incomplete = true
return &perr
Expand Down Expand Up @@ -1329,7 +1397,7 @@ func disallowVendorVisibility(srcDir string, p *Package, stk *ImportStack) *Pack
perr := *p
perr.Error = &PackageError{
ImportStack: stk.Copy(),
Err: "use of vendored package not allowed",
Err: errors.New("use of vendored package not allowed"),
}
perr.Incomplete = true
return &perr
Expand Down Expand Up @@ -1455,7 +1523,7 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
err = base.ExpandScanner(err)
p.Error = &PackageError{
ImportStack: stk.Copy(),
Err: err.Error(),
Err: err,
}
return
}
Expand All @@ -1472,7 +1540,7 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
// Report an error when the old code.google.com/p/go.tools paths are used.
if InstallTargetDir(p) == StalePath {
newPath := strings.Replace(p.ImportPath, "code.google.com/p/go.", "golang.org/x/", 1)
e := fmt.Sprintf("the %v command has moved; use %v instead.", p.ImportPath, newPath)
e := ImportErrorf(p.ImportPath, "the %v command has moved; use %v instead.", p.ImportPath, newPath)
p.Error = &PackageError{Err: e}
return
}
Expand Down Expand Up @@ -1585,7 +1653,7 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
if f1 != "" {
p.Error = &PackageError{
ImportStack: stk.Copy(),
Err: fmt.Sprintf("case-insensitive file name collision: %q and %q", f1, f2),
Err: fmt.Errorf("case-insensitive file name collision: %q and %q", f1, f2),
}
return
}
Expand All @@ -1601,22 +1669,22 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
if !SafeArg(file) || strings.HasPrefix(file, "_cgo_") {
p.Error = &PackageError{
ImportStack: stk.Copy(),
Err: fmt.Sprintf("invalid input file name %q", file),
Err: fmt.Errorf("invalid input file name %q", file),
}
return
}
}
if name := pathpkg.Base(p.ImportPath); !SafeArg(name) {
p.Error = &PackageError{
ImportStack: stk.Copy(),
Err: fmt.Sprintf("invalid input directory name %q", name),
Err: fmt.Errorf("invalid input directory name %q", name),
}
return
}
if !SafeArg(p.ImportPath) {
p.Error = &PackageError{
ImportStack: stk.Copy(),
Err: fmt.Sprintf("invalid import path %q", p.ImportPath),
Err: ImportErrorf(p.ImportPath, "invalid import path %q", p.ImportPath),
}
return
}
Expand Down Expand Up @@ -1662,31 +1730,31 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
// code; see issue #16050).
}

setError := func(msg string) {
setError := func(err error) {
p.Error = &PackageError{
ImportStack: stk.Copy(),
Err: msg,
Err: err,
}
}

// The gc toolchain only permits C source files with cgo or SWIG.
if len(p.CFiles) > 0 && !p.UsesCgo() && !p.UsesSwig() && cfg.BuildContext.Compiler == "gc" {
setError(fmt.Sprintf("C source files not allowed when not using cgo or SWIG: %s", strings.Join(p.CFiles, " ")))
setError(fmt.Errorf("C source files not allowed when not using cgo or SWIG: %s", strings.Join(p.CFiles, " ")))
return
}

// C++, Objective-C, and Fortran source files are permitted only with cgo or SWIG,
// regardless of toolchain.
if len(p.CXXFiles) > 0 && !p.UsesCgo() && !p.UsesSwig() {
setError(fmt.Sprintf("C++ source files not allowed when not using cgo or SWIG: %s", strings.Join(p.CXXFiles, " ")))
setError(fmt.Errorf("C++ source files not allowed when not using cgo or SWIG: %s", strings.Join(p.CXXFiles, " ")))
return
}
if len(p.MFiles) > 0 && !p.UsesCgo() && !p.UsesSwig() {
setError(fmt.Sprintf("Objective-C source files not allowed when not using cgo or SWIG: %s", strings.Join(p.MFiles, " ")))
setError(fmt.Errorf("Objective-C source files not allowed when not using cgo or SWIG: %s", strings.Join(p.MFiles, " ")))
return
}
if len(p.FFiles) > 0 && !p.UsesCgo() && !p.UsesSwig() {
setError(fmt.Sprintf("Fortran source files not allowed when not using cgo or SWIG: %s", strings.Join(p.FFiles, " ")))
setError(fmt.Errorf("Fortran source files not allowed when not using cgo or SWIG: %s", strings.Join(p.FFiles, " ")))
return
}

Expand All @@ -1695,7 +1763,7 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
if other := foldPath[fold]; other == "" {
foldPath[fold] = p.ImportPath
} else if other != p.ImportPath {
setError(fmt.Sprintf("case-insensitive import collision: %q and %q", p.ImportPath, other))
setError(ImportErrorf(p.ImportPath, "case-insensitive import collision: %q and %q", p.ImportPath, other))
return
}

Expand Down Expand Up @@ -2102,7 +2170,7 @@ func GoFilesPackage(gofiles []string) *Package {
pkg.Internal.CmdlineFiles = true
pkg.Name = f
pkg.Error = &PackageError{
Err: fmt.Sprintf("named files must be .go files: %s", pkg.Name),
Err: fmt.Errorf("named files must be .go files: %s", pkg.Name),
}
return pkg
}
Expand Down
6 changes: 3 additions & 3 deletions src/cmd/go/internal/load/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func TestPackagesAndErrors(p *Package, cover *TestCover) (pmain, ptest, pxtest *
// non-test copy of a package.
ptestErr = &PackageError{
ImportStack: testImportStack(stk[0], p1, p.ImportPath),
Err: "import cycle not allowed in test",
Err: errors.New("import cycle not allowed in test"),
IsImportCycle: true,
}
}
Expand Down Expand Up @@ -271,7 +271,7 @@ func TestPackagesAndErrors(p *Package, cover *TestCover) (pmain, ptest, pxtest *
// afterward that gathers t.Cover information.
t, err := loadTestFuncs(ptest)
if err != nil && pmain.Error == nil {
pmain.Error = &PackageError{Err: err.Error()}
pmain.Error = &PackageError{Err: err}
}
t.Cover = cover
if len(ptest.GoFiles)+len(ptest.CgoFiles) > 0 {
Expand Down Expand Up @@ -322,7 +322,7 @@ func TestPackagesAndErrors(p *Package, cover *TestCover) (pmain, ptest, pxtest *

data, err := formatTestmain(t)
if err != nil && pmain.Error == nil {
pmain.Error = &PackageError{Err: err.Error()}
pmain.Error = &PackageError{Err: err}
}
if data != nil {
pmain.Internal.TestmainGo = &data
Expand Down
Loading

0 comments on commit 32b6eb8

Please sign in to comment.