Skip to content

Commit 32b6eb8

Browse files
author
Jay Conrod
committed
cmd/go: eliminate redundancy in import error messages
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 <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent b3104fe commit 32b6eb8

File tree

9 files changed

+167
-55
lines changed

9 files changed

+167
-55
lines changed

src/cmd/go/internal/fmtcmd/fmt.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func runFmt(cmd *base.Command, args []string) {
7272
continue
7373
}
7474
if pkg.Error != nil {
75-
if strings.HasPrefix(pkg.Error.Err, "build constraints exclude all Go files") {
75+
if strings.HasPrefix(pkg.Error.Err.Error(), "build constraints exclude all Go files") {
7676
// Skip this error, as we will format
7777
// all files regardless.
7878
} else {

src/cmd/go/internal/get/get.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ func download(arg string, parent *load.Package, stk *load.ImportStack, mode int)
274274
stk.Push(arg)
275275
err := downloadPackage(p)
276276
if err != nil {
277-
base.Errorf("%s", &load.PackageError{ImportStack: stk.Copy(), Err: err.Error()})
277+
base.Errorf("%s", &load.PackageError{ImportStack: stk.Copy(), Err: err})
278278
stk.Pop()
279279
return
280280
}
@@ -355,7 +355,7 @@ func download(arg string, parent *load.Package, stk *load.ImportStack, mode int)
355355
stk.Push(path)
356356
err := &load.PackageError{
357357
ImportStack: stk.Copy(),
358-
Err: "must be imported as " + path[j+len("vendor/"):],
358+
Err: load.ImportErrorf(path, "%s must be imported as %s", path, path[j+len("vendor/"):]),
359359
}
360360
stk.Pop()
361361
base.Errorf("%s", err)

src/cmd/go/internal/load/pkg.go

+99-31
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package load
77

88
import (
99
"bytes"
10+
"encoding/json"
1011
"errors"
1112
"fmt"
1213
"go/build"
@@ -304,9 +305,9 @@ func (p *Package) copyBuild(pp *build.Package) {
304305
type PackageError struct {
305306
ImportStack []string // shortest path from package named on command line to this one
306307
Pos string // position of error
307-
Err string // the error itself
308-
IsImportCycle bool `json:"-"` // the error is an import cycle
309-
Hard bool `json:"-"` // whether the error is soft or hard; soft errors are ignored in some places
308+
Err error // the error itself
309+
IsImportCycle bool // the error is an import cycle
310+
Hard bool // whether the error is soft or hard; soft errors are ignored in some places
310311
}
311312

312313
func (p *PackageError) Error() string {
@@ -317,12 +318,77 @@ func (p *PackageError) Error() string {
317318
if p.Pos != "" {
318319
// Omit import stack. The full path to the file where the error
319320
// is the most important thing.
320-
return p.Pos + ": " + p.Err
321+
return p.Pos + ": " + p.Err.Error()
321322
}
322-
if len(p.ImportStack) == 0 {
323-
return p.Err
323+
324+
// If the error is an ImportPathError, and the last path on the stack appears
325+
// in the error message, omit that path from the stack to avoid repetition.
326+
// If an ImportPathError wraps another ImportPathError that matches the
327+
// last path on the stack, we don't omit the path. An error like
328+
// "package A imports B: error loading C caused by B" would not be clearer
329+
// if "imports B" were omitted.
330+
stack := p.ImportStack
331+
var ierr ImportPathError
332+
if len(stack) > 0 && errors.As(p.Err, &ierr) && ierr.ImportPath() == stack[len(stack)-1] {
333+
stack = stack[:len(stack)-1]
334+
}
335+
if len(stack) == 0 {
336+
return p.Err.Error()
337+
}
338+
return "package " + strings.Join(stack, "\n\timports ") + ": " + p.Err.Error()
339+
}
340+
341+
// PackageError implements MarshalJSON so that Err is marshaled as a string
342+
// and non-essential fields are omitted.
343+
func (p *PackageError) MarshalJSON() ([]byte, error) {
344+
perr := struct {
345+
ImportStack []string
346+
Pos string
347+
Err string
348+
}{p.ImportStack, p.Pos, p.Err.Error()}
349+
return json.Marshal(perr)
350+
}
351+
352+
// ImportPathError is a type of error that prevents a package from being loaded
353+
// for a given import path. When such a package is loaded, a *Package is
354+
// returned with Err wrapping an ImportPathError: the error is attached to
355+
// the imported package, not the importing package.
356+
//
357+
// The string returned by ImportPath must appear in the string returned by
358+
// Error. Errors that wrap ImportPathError (such as PackageError) may omit
359+
// the import path.
360+
type ImportPathError interface {
361+
error
362+
ImportPath() string
363+
}
364+
365+
type importError struct {
366+
importPath string
367+
err error // created with fmt.Errorf
368+
}
369+
370+
var _ ImportPathError = (*importError)(nil)
371+
372+
func ImportErrorf(path, format string, args ...interface{}) ImportPathError {
373+
err := &importError{importPath: path, err: fmt.Errorf(format, args...)}
374+
if errStr := err.Error(); !strings.Contains(errStr, path) {
375+
panic(fmt.Sprintf("path %q not in error %q", path, errStr))
324376
}
325-
return "package " + strings.Join(p.ImportStack, "\n\timports ") + ": " + p.Err
377+
return err
378+
}
379+
380+
func (e *importError) Error() string {
381+
return e.err.Error()
382+
}
383+
384+
func (e *importError) Unwrap() error {
385+
// Don't return e.err directly, since we're only wrapping an error if %w
386+
// was passed to ImportErrorf.
387+
return errors.Unwrap(e.err)
388+
}
389+
390+
func (e *importError) ImportPath() string {
391+
return e.importPath
326392
}
327393

328394
// An ImportStack is a stack of import paths, possibly with the suffix " (test)" appended.
@@ -489,7 +555,7 @@ func loadImport(pre *preload, path, srcDir string, parent *Package, stk *ImportS
489555
ImportPath: path,
490556
Error: &PackageError{
491557
ImportStack: stk.Copy(),
492-
Err: err.Error(),
558+
Err: err,
493559
},
494560
},
495561
}
@@ -516,7 +582,7 @@ func loadImport(pre *preload, path, srcDir string, parent *Package, stk *ImportS
516582
if !cfg.ModulesEnabled && path != cleanImport(path) {
517583
p.Error = &PackageError{
518584
ImportStack: stk.Copy(),
519-
Err: fmt.Sprintf("non-canonical import path: %q should be %q", path, pathpkg.Clean(path)),
585+
Err: fmt.Errorf("non-canonical import path: %q should be %q", path, pathpkg.Clean(path)),
520586
}
521587
p.Incomplete = true
522588
}
@@ -536,20 +602,22 @@ func loadImport(pre *preload, path, srcDir string, parent *Package, stk *ImportS
536602
perr := *p
537603
perr.Error = &PackageError{
538604
ImportStack: stk.Copy(),
539-
Err: fmt.Sprintf("import %q is a program, not an importable package", path),
605+
Err: ImportErrorf(path, "import %q is a program, not an importable package", path),
540606
}
541607
return setErrorPos(&perr, importPos)
542608
}
543609

544610
if p.Internal.Local && parent != nil && !parent.Internal.Local {
545611
perr := *p
546-
errMsg := fmt.Sprintf("local import %q in non-local package", path)
612+
var err error
547613
if path == "." {
548-
errMsg = "cannot import current directory"
614+
err = ImportErrorf(path, "%s: cannot import current directory", path)
615+
} else {
616+
err = ImportErrorf(path, "local import %q in non-local package", path)
549617
}
550618
perr.Error = &PackageError{
551619
ImportStack: stk.Copy(),
552-
Err: errMsg,
620+
Err: err,
553621
}
554622
return setErrorPos(&perr, importPos)
555623
}
@@ -1125,7 +1193,7 @@ func reusePackage(p *Package, stk *ImportStack) *Package {
11251193
if p.Error == nil {
11261194
p.Error = &PackageError{
11271195
ImportStack: stk.Copy(),
1128-
Err: "import cycle not allowed",
1196+
Err: errors.New("import cycle not allowed"),
11291197
IsImportCycle: true,
11301198
}
11311199
}
@@ -1228,7 +1296,7 @@ func disallowInternal(srcDir string, importer *Package, importerPath string, p *
12281296
perr := *p
12291297
perr.Error = &PackageError{
12301298
ImportStack: stk.Copy(),
1231-
Err: "use of internal package " + p.ImportPath + " not allowed",
1299+
Err: ImportErrorf(p.ImportPath, "use of internal package "+p.ImportPath+" not allowed"),
12321300
}
12331301
perr.Incomplete = true
12341302
return &perr
@@ -1275,7 +1343,7 @@ func disallowVendor(srcDir string, importer *Package, importerPath, path string,
12751343
perr := *p
12761344
perr.Error = &PackageError{
12771345
ImportStack: stk.Copy(),
1278-
Err: "must be imported as " + path[i+len("vendor/"):],
1346+
Err: ImportErrorf(path, "%s must be imported as %s", path, path[i+len("vendor/"):]),
12791347
}
12801348
perr.Incomplete = true
12811349
return &perr
@@ -1329,7 +1397,7 @@ func disallowVendorVisibility(srcDir string, p *Package, stk *ImportStack) *Pack
13291397
perr := *p
13301398
perr.Error = &PackageError{
13311399
ImportStack: stk.Copy(),
1332-
Err: "use of vendored package not allowed",
1400+
Err: errors.New("use of vendored package not allowed"),
13331401
}
13341402
perr.Incomplete = true
13351403
return &perr
@@ -1455,7 +1523,7 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
14551523
err = base.ExpandScanner(err)
14561524
p.Error = &PackageError{
14571525
ImportStack: stk.Copy(),
1458-
Err: err.Error(),
1526+
Err: err,
14591527
}
14601528
return
14611529
}
@@ -1472,7 +1540,7 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
14721540
// Report an error when the old code.google.com/p/go.tools paths are used.
14731541
if InstallTargetDir(p) == StalePath {
14741542
newPath := strings.Replace(p.ImportPath, "code.google.com/p/go.", "golang.org/x/", 1)
1475-
e := fmt.Sprintf("the %v command has moved; use %v instead.", p.ImportPath, newPath)
1543+
e := ImportErrorf(p.ImportPath, "the %v command has moved; use %v instead.", p.ImportPath, newPath)
14761544
p.Error = &PackageError{Err: e}
14771545
return
14781546
}
@@ -1585,7 +1653,7 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
15851653
if f1 != "" {
15861654
p.Error = &PackageError{
15871655
ImportStack: stk.Copy(),
1588-
Err: fmt.Sprintf("case-insensitive file name collision: %q and %q", f1, f2),
1656+
Err: fmt.Errorf("case-insensitive file name collision: %q and %q", f1, f2),
15891657
}
15901658
return
15911659
}
@@ -1601,22 +1669,22 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
16011669
if !SafeArg(file) || strings.HasPrefix(file, "_cgo_") {
16021670
p.Error = &PackageError{
16031671
ImportStack: stk.Copy(),
1604-
Err: fmt.Sprintf("invalid input file name %q", file),
1672+
Err: fmt.Errorf("invalid input file name %q", file),
16051673
}
16061674
return
16071675
}
16081676
}
16091677
if name := pathpkg.Base(p.ImportPath); !SafeArg(name) {
16101678
p.Error = &PackageError{
16111679
ImportStack: stk.Copy(),
1612-
Err: fmt.Sprintf("invalid input directory name %q", name),
1680+
Err: fmt.Errorf("invalid input directory name %q", name),
16131681
}
16141682
return
16151683
}
16161684
if !SafeArg(p.ImportPath) {
16171685
p.Error = &PackageError{
16181686
ImportStack: stk.Copy(),
1619-
Err: fmt.Sprintf("invalid import path %q", p.ImportPath),
1687+
Err: ImportErrorf(p.ImportPath, "invalid import path %q", p.ImportPath),
16201688
}
16211689
return
16221690
}
@@ -1662,31 +1730,31 @@ func (p *Package) load(stk *ImportStack, bp *build.Package, err error) {
16621730
// code; see issue #16050).
16631731
}
16641732

1665-
setError := func(msg string) {
1733+
setError := func(err error) {
16661734
p.Error = &PackageError{
16671735
ImportStack: stk.Copy(),
1668-
Err: msg,
1736+
Err: err,
16691737
}
16701738
}
16711739

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

16781746
// C++, Objective-C, and Fortran source files are permitted only with cgo or SWIG,
16791747
// regardless of toolchain.
16801748
if len(p.CXXFiles) > 0 && !p.UsesCgo() && !p.UsesSwig() {
1681-
setError(fmt.Sprintf("C++ source files not allowed when not using cgo or SWIG: %s", strings.Join(p.CXXFiles, " ")))
1749+
setError(fmt.Errorf("C++ source files not allowed when not using cgo or SWIG: %s", strings.Join(p.CXXFiles, " ")))
16821750
return
16831751
}
16841752
if len(p.MFiles) > 0 && !p.UsesCgo() && !p.UsesSwig() {
1685-
setError(fmt.Sprintf("Objective-C source files not allowed when not using cgo or SWIG: %s", strings.Join(p.MFiles, " ")))
1753+
setError(fmt.Errorf("Objective-C source files not allowed when not using cgo or SWIG: %s", strings.Join(p.MFiles, " ")))
16861754
return
16871755
}
16881756
if len(p.FFiles) > 0 && !p.UsesCgo() && !p.UsesSwig() {
1689-
setError(fmt.Sprintf("Fortran source files not allowed when not using cgo or SWIG: %s", strings.Join(p.FFiles, " ")))
1757+
setError(fmt.Errorf("Fortran source files not allowed when not using cgo or SWIG: %s", strings.Join(p.FFiles, " ")))
16901758
return
16911759
}
16921760

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

@@ -2102,7 +2170,7 @@ func GoFilesPackage(gofiles []string) *Package {
21022170
pkg.Internal.CmdlineFiles = true
21032171
pkg.Name = f
21042172
pkg.Error = &PackageError{
2105-
Err: fmt.Sprintf("named files must be .go files: %s", pkg.Name),
2173+
Err: fmt.Errorf("named files must be .go files: %s", pkg.Name),
21062174
}
21072175
return pkg
21082176
}

src/cmd/go/internal/load/test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func TestPackagesAndErrors(p *Package, cover *TestCover) (pmain, ptest, pxtest *
110110
// non-test copy of a package.
111111
ptestErr = &PackageError{
112112
ImportStack: testImportStack(stk[0], p1, p.ImportPath),
113-
Err: "import cycle not allowed in test",
113+
Err: errors.New("import cycle not allowed in test"),
114114
IsImportCycle: true,
115115
}
116116
}
@@ -271,7 +271,7 @@ func TestPackagesAndErrors(p *Package, cover *TestCover) (pmain, ptest, pxtest *
271271
// afterward that gathers t.Cover information.
272272
t, err := loadTestFuncs(ptest)
273273
if err != nil && pmain.Error == nil {
274-
pmain.Error = &PackageError{Err: err.Error()}
274+
pmain.Error = &PackageError{Err: err}
275275
}
276276
t.Cover = cover
277277
if len(ptest.GoFiles)+len(ptest.CgoFiles) > 0 {
@@ -322,7 +322,7 @@ func TestPackagesAndErrors(p *Package, cover *TestCover) (pmain, ptest, pxtest *
322322

323323
data, err := formatTestmain(t)
324324
if err != nil && pmain.Error == nil {
325-
pmain.Error = &PackageError{Err: err.Error()}
325+
pmain.Error = &PackageError{Err: err}
326326
}
327327
if data != nil {
328328
pmain.Internal.TestmainGo = &data

0 commit comments

Comments
 (0)