Skip to content
This repository was archived by the owner on Sep 9, 2020. It is now read-only.

Commit 3179508

Browse files
committed
Return "error map" from PackageTree.ToReachMap()
This second parameter provides information about why a package was dropped from the ReachMap - either what problem it had itself, or the problem in one of the internal packages it transitively imports.
1 parent c882f60 commit 3179508

File tree

6 files changed

+243
-68
lines changed

6 files changed

+243
-68
lines changed

analysis.go

Lines changed: 131 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,34 @@ type ReachMap map[string]struct {
365365
Internal, External []string
366366
}
367367

368+
// ProblemImportError describes the reason that a particular import path is
369+
// not safely importable.
370+
type ProblemImportError struct {
371+
// The import path of the package with some problem rendering it
372+
// unimportable.
373+
ImportPath string
374+
// The path to the internal package the problem package imports that is the
375+
// original cause of this issue. If empty, the package itself is the
376+
// problem.
377+
Cause []string
378+
// The actual error from ListPackages that is undermining importability for
379+
// this package.
380+
Err error
381+
}
382+
383+
// Error formats the ProblemImportError as a string, reflecting whether the
384+
// error represents a direct or transitive problem.
385+
func (e *ProblemImportError) Error() string {
386+
switch len(e.Cause) {
387+
case 0:
388+
return fmt.Sprintf("%q contains malformed code: %s", e.ImportPath, e.Err.Error())
389+
case 1:
390+
return fmt.Sprintf("%q imports %q, which contains malformed code: %s", e.ImportPath, e.Cause[0], e.Err.Error())
391+
default:
392+
return fmt.Sprintf("%q transitively (through %v packages) imports %q, which contains malformed code: %s", e.ImportPath, len(e.Cause)-1, e.Cause[len(e.Cause)-1], e.Err.Error())
393+
}
394+
}
395+
368396
// ToReachMap looks through a PackageTree and computes the list of external
369397
// import statements (that is, import statements pointing to packages that are
370398
// not logical children of PackageTree.ImportRoot) that are transitively
@@ -437,7 +465,7 @@ type ReachMap map[string]struct {
437465
//
438466
// When backprop is false, errors in internal packages are functionally
439467
// identical to ignoring that package.
440-
func (t PackageTree) ToReachMap(main, tests bool, ignore map[string]bool) ReachMap {
468+
func (t PackageTree) ToReachMap(main, tests bool, ignore map[string]bool) (ReachMap, map[string]*ProblemImportError) {
441469
if ignore == nil {
442470
ignore = make(map[string]bool)
443471
}
@@ -499,6 +527,11 @@ func (t PackageTree) ToReachMap(main, tests bool, ignore map[string]bool) ReachM
499527
//return wmToReachNoPoison(wm)
500528
}
501529

530+
// Helper func to create an error when a package is missing.
531+
func missingPkgErr(pkg string) error {
532+
return fmt.Errorf("no package exists at %q", pkg)
533+
}
534+
502535
// wmToReach takes an internal "workmap" constructed by
503536
// PackageTree.ExternalReach(), transitively walks (via depth-first traversal)
504537
// all internal imports until they reach an external path or terminate, then
@@ -511,7 +544,7 @@ func (t PackageTree) ToReachMap(main, tests bool, ignore map[string]bool) ReachM
511544
//
512545
// The basedir string, with a trailing slash ensured, will be stripped from the
513546
// keys of the returned map.
514-
func wmToReach(workmap map[string]wm) ReachMap {
547+
func wmToReach(workmap map[string]wm) (ReachMap, map[string]*ProblemImportError) {
515548
// Uses depth-first exploration to compute reachability into external
516549
// packages, dropping any internal packages on "poisoned paths" - a path
517550
// containing a package with an error, or with a dep on an internal package
@@ -526,15 +559,89 @@ func wmToReach(workmap map[string]wm) ReachMap {
526559
colors := make(map[string]uint8)
527560
exrsets := make(map[string]map[string]struct{})
528561
inrsets := make(map[string]map[string]struct{})
562+
errmap := make(map[string]*ProblemImportError)
529563

530-
// poison is a helper func to eliminate specific reachsets from exrsets
531-
poison := func(path []string) {
532-
for _, ppkg := range path {
564+
// poison is a helper func to eliminate specific reachsets from exrsets and
565+
// inrsets, and populate error information along the way.
566+
poison := func(path []string, err *ProblemImportError) {
567+
for k, ppkg := range path {
533568
delete(exrsets, ppkg)
534569
delete(inrsets, ppkg)
570+
571+
// Duplicate the err for this package
572+
kerr := &ProblemImportError{
573+
ImportPath: ppkg,
574+
Err: err.Err,
575+
}
576+
577+
// Shift the slice bounds on the incoming err.Cause.
578+
//
579+
// This check will only not be true on the final path element when
580+
// entering via poisonWhite, where the last pkg is the underlying
581+
// cause of the problem, and is thus expected to have an empty Cause
582+
// slice.
583+
if k+1 < len(err.Cause) {
584+
// reuse the slice
585+
kerr.Cause = err.Cause[k+1:]
586+
}
587+
588+
// Both black and white cases can have the final element be a
589+
// package that doesn't exist. If that's the case, don't write it
590+
// directly to the errmap, as presence in the errmap indicates the
591+
// package was present in the input PackageTree.
592+
if k == len(path)-1 {
593+
if _, exists := workmap[path[len(path)-1]]; !exists {
594+
continue
595+
}
596+
}
597+
598+
// Direct writing to the errmap means that if multiple errors affect
599+
// a given package, only the last error visited will be reported.
600+
// But that should be sufficient; presumably, the user can
601+
// iteratively resolve the errors.
602+
errmap[ppkg] = kerr
535603
}
536604
}
537605

606+
// poisonWhite wraps poison for error recording in the white-poisoning case,
607+
// where we're constructing a new poison path.
608+
poisonWhite := func(path []string) {
609+
err := &ProblemImportError{
610+
Cause: make([]string, len(path)),
611+
}
612+
copy(err.Cause, path)
613+
614+
// find the tail err
615+
tail := path[len(path)-1]
616+
if w, exists := workmap[tail]; exists {
617+
// If we make it to here, the dfe guarantees that the workmap
618+
// will contain an error for this pkg.
619+
err.Err = w.err
620+
} else {
621+
err.Err = missingPkgErr(tail)
622+
}
623+
624+
poison(path, err)
625+
}
626+
// poisonBlack wraps poison for error recording in the black-poisoning case,
627+
// where we're connecting to an existing poison path.
628+
poisonBlack := func(path []string, from string) {
629+
// Because the outer dfe loop ensures we never directly re-visit a pkg
630+
// that was already completed (black), we don't have to defend against
631+
// an empty path here.
632+
633+
fromErr := errmap[from]
634+
err := &ProblemImportError{
635+
Err: fromErr.Err,
636+
Cause: make([]string, 0, len(path)+len(fromErr.Cause)+1),
637+
}
638+
err.Cause = append(err.Cause, path...)
639+
err.Cause = append(err.Cause, from)
640+
err.Cause = append(err.Cause, fromErr.Cause...)
641+
642+
poison(path, err)
643+
}
644+
538645
var dfe func(string, []string) bool
539646

540647
// dfe is the depth-first-explorer that computes a safe, error-free external
@@ -554,9 +661,15 @@ func wmToReach(workmap map[string]wm) ReachMap {
554661

555662
// make sure it's present and w/out errs
556663
w, exists := workmap[pkg]
664+
665+
// Push current visitee onto onto the path slice. Passing this as a
666+
// value has the effect of auto-popping the slice, while also giving
667+
// us safe memory reuse.
668+
path = append(path, pkg)
669+
557670
if !exists || w.err != nil {
558671
// Does not exist or has an err; poison self and all parents
559-
poison(path)
672+
poisonWhite(path)
560673

561674
// we know we're done here, so mark it black
562675
colors[pkg] = black
@@ -566,11 +679,6 @@ func wmToReach(workmap map[string]wm) ReachMap {
566679
rs := make(map[string]struct{})
567680
irs := make(map[string]struct{})
568681

569-
// Push self onto the path slice. Passing this as a value has the
570-
// effect of auto-popping the slice, while also giving us safe
571-
// memory reuse.
572-
path = append(path, pkg)
573-
574682
// Dump this package's external pkgs into its own reachset. Separate
575683
// loop from the parent dump to avoid nested map loop lookups.
576684
for ex := range w.ex {
@@ -647,13 +755,14 @@ func wmToReach(workmap map[string]wm) ReachMap {
647755
//poison(append(path, pkg)) // poison self and parents
648756

649757
case black:
650-
// black means we're done with the package. If it has an entry in
651-
// exrsets, it completed successfully. If not, it was poisoned,
652-
// and we need to bubble the poison back up.
758+
// black means we're revisiting a package that was already
759+
// completely explored. If it has an entry in exrsets, it completed
760+
// successfully. If not, it was poisoned, and we need to bubble the
761+
// poison back up.
653762
rs, exists := exrsets[pkg]
654763
if !exists {
655764
// just poison parents; self was necessarily already poisoned
656-
poison(path)
765+
poisonBlack(path, pkg)
657766
return false
658767
}
659768
// If external reachset existed, internal must (even if empty)
@@ -687,7 +796,12 @@ func wmToReach(workmap map[string]wm) ReachMap {
687796
// comparably well, and fits nicely with an escape hatch in the dfe.
688797
var path []string
689798
for pkg := range workmap {
690-
dfe(pkg, path)
799+
// However, at least check that the package isn't already fully visited;
800+
// this saves a bit of time and implementation complexity inside the
801+
// closures.
802+
if colors[pkg] != black {
803+
dfe(pkg, path)
804+
}
691805
}
692806

693807
type ie struct {
@@ -734,7 +848,7 @@ func wmToReach(workmap map[string]wm) ReachMap {
734848
rm[pkg] = sets
735849
}
736850

737-
return rm
851+
return rm, errmap
738852
}
739853

740854
// FlattenAll flattens a reachmap into a sorted, deduplicated list of all the

0 commit comments

Comments
 (0)