@@ -365,6 +365,34 @@ type ReachMap map[string]struct {
365
365
Internal , External []string
366
366
}
367
367
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
+
368
396
// ToReachMap looks through a PackageTree and computes the list of external
369
397
// import statements (that is, import statements pointing to packages that are
370
398
// not logical children of PackageTree.ImportRoot) that are transitively
@@ -437,7 +465,7 @@ type ReachMap map[string]struct {
437
465
//
438
466
// When backprop is false, errors in internal packages are functionally
439
467
// 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 ) {
441
469
if ignore == nil {
442
470
ignore = make (map [string ]bool )
443
471
}
@@ -499,6 +527,11 @@ func (t PackageTree) ToReachMap(main, tests bool, ignore map[string]bool) ReachM
499
527
//return wmToReachNoPoison(wm)
500
528
}
501
529
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
+
502
535
// wmToReach takes an internal "workmap" constructed by
503
536
// PackageTree.ExternalReach(), transitively walks (via depth-first traversal)
504
537
// 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
511
544
//
512
545
// The basedir string, with a trailing slash ensured, will be stripped from the
513
546
// keys of the returned map.
514
- func wmToReach (workmap map [string ]wm ) ReachMap {
547
+ func wmToReach (workmap map [string ]wm ) ( ReachMap , map [ string ] * ProblemImportError ) {
515
548
// Uses depth-first exploration to compute reachability into external
516
549
// packages, dropping any internal packages on "poisoned paths" - a path
517
550
// 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 {
526
559
colors := make (map [string ]uint8 )
527
560
exrsets := make (map [string ]map [string ]struct {})
528
561
inrsets := make (map [string ]map [string ]struct {})
562
+ errmap := make (map [string ]* ProblemImportError )
529
563
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 {
533
568
delete (exrsets , ppkg )
534
569
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
535
603
}
536
604
}
537
605
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
+
538
645
var dfe func (string , []string ) bool
539
646
540
647
// dfe is the depth-first-explorer that computes a safe, error-free external
@@ -554,9 +661,15 @@ func wmToReach(workmap map[string]wm) ReachMap {
554
661
555
662
// make sure it's present and w/out errs
556
663
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
+
557
670
if ! exists || w .err != nil {
558
671
// Does not exist or has an err; poison self and all parents
559
- poison (path )
672
+ poisonWhite (path )
560
673
561
674
// we know we're done here, so mark it black
562
675
colors [pkg ] = black
@@ -566,11 +679,6 @@ func wmToReach(workmap map[string]wm) ReachMap {
566
679
rs := make (map [string ]struct {})
567
680
irs := make (map [string ]struct {})
568
681
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
-
574
682
// Dump this package's external pkgs into its own reachset. Separate
575
683
// loop from the parent dump to avoid nested map loop lookups.
576
684
for ex := range w .ex {
@@ -647,13 +755,14 @@ func wmToReach(workmap map[string]wm) ReachMap {
647
755
//poison(append(path, pkg)) // poison self and parents
648
756
649
757
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.
653
762
rs , exists := exrsets [pkg ]
654
763
if ! exists {
655
764
// just poison parents; self was necessarily already poisoned
656
- poison (path )
765
+ poisonBlack (path , pkg )
657
766
return false
658
767
}
659
768
// If external reachset existed, internal must (even if empty)
@@ -687,7 +796,12 @@ func wmToReach(workmap map[string]wm) ReachMap {
687
796
// comparably well, and fits nicely with an escape hatch in the dfe.
688
797
var path []string
689
798
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
+ }
691
805
}
692
806
693
807
type ie struct {
@@ -734,7 +848,7 @@ func wmToReach(workmap map[string]wm) ReachMap {
734
848
rm [pkg ] = sets
735
849
}
736
850
737
- return rm
851
+ return rm , errmap
738
852
}
739
853
740
854
// FlattenAll flattens a reachmap into a sorted, deduplicated list of all the
0 commit comments