@@ -548,7 +548,13 @@ func (b *typeCheckBatch) importPackage(ctx context.Context, mp *metadata.Package
548548 }
549549 }
550550 } else {
551- id = importLookup (PackagePath (item .Path ))
551+ var alt PackageID
552+ id , alt = importLookup (PackagePath (item .Path ))
553+ if alt != "" {
554+ // Any bug leading to this scenario would have already been reported
555+ // in importLookup.
556+ return fmt .Errorf ("inconsistent metadata during import: for package path %q, found both IDs %q and %q" , item .Path , id , alt )
557+ }
552558 var err error
553559 pkg , err = b .getImportPackage (ctx , id )
554560 if err != nil {
@@ -665,8 +671,12 @@ func (b *typeCheckBatch) checkPackageForImport(ctx context.Context, ph *packageH
665671// a given package path, based on the forward transitive closure of the initial
666672// package (id).
667673//
674+ // If the second result is non-empty, it is another ID discovered in the import
675+ // graph for the same package path. This means the import graph is
676+ // incoherent--see #63822 and the long comment below.
677+ //
668678// The resulting function is not concurrency safe.
669- func importLookup (mp * metadata.Package , source metadata.Source ) func (PackagePath ) PackageID {
679+ func importLookup (mp * metadata.Package , source metadata.Source ) func (PackagePath ) ( id , altID PackageID ) {
670680 assert (mp != nil , "nil metadata" )
671681
672682 // This function implements an incremental depth first scan through the
@@ -680,6 +690,10 @@ func importLookup(mp *metadata.Package, source metadata.Source) func(PackagePath
680690 mp .PkgPath : mp .ID ,
681691 }
682692
693+ // altIDs records alternative IDs for the given path, to report inconsistent
694+ // metadata.
695+ var altIDs map [PackagePath ]PackageID
696+
683697 // pending is a FIFO queue of package metadata that has yet to have its
684698 // dependencies fully scanned.
685699 // Invariant: all entries in pending are already mapped in impMap.
@@ -695,13 +709,82 @@ func importLookup(mp *metadata.Package, source metadata.Source) func(PackagePath
695709 if prevID , ok := impMap [depPath ]; ok {
696710 // debugging #63822
697711 if prevID != depID {
712+ if altIDs == nil {
713+ altIDs = make (map [PackagePath ]PackageID )
714+ }
715+ if _ , ok := altIDs [depPath ]; ! ok {
716+ altIDs [depPath ] = depID
717+ }
698718 prev := source .Metadata (prevID )
699719 curr := source .Metadata (depID )
700720 switch {
701721 case prev == nil || curr == nil :
702722 bug .Reportf ("inconsistent view of dependencies (missing dep)" )
703723 case prev .ForTest != curr .ForTest :
704- bug .Reportf ("inconsistent view of dependencies (mismatching ForTest)" )
724+ // This case is unfortunately understood to be possible.
725+ //
726+ // To explain this, consider a package a_test testing the package
727+ // a, and for brevity denote by b' the intermediate test variant of
728+ // the package b, which is created for the import graph of a_test,
729+ // if b imports a.
730+ //
731+ // Now imagine that we have the following import graph, where
732+ // higher packages import lower ones.
733+ //
734+ // a_test
735+ // / \
736+ // b' c
737+ // / \ /
738+ // a d
739+ //
740+ // In this graph, there is one intermediate test variant b',
741+ // because b imports a and so b' must hold the test variant import.
742+ //
743+ // Now, imagine that an on-disk change (perhaps due to a branch
744+ // switch) affects the above import graph such that d imports a.
745+ //
746+ // a_test
747+ // / \
748+ // b' c*
749+ // / \ /
750+ // / d*
751+ // a---/
752+ //
753+ // In this case, c and d should really be intermediate test
754+ // variants, because they reach a. However, suppose that gopls does
755+ // not know this yet (as indicated by '*').
756+ //
757+ // Now suppose that the metadata of package c is invalidated, for
758+ // example due to a change in an unrelated import or an added file.
759+ // This will invalidate the metadata of c and a_test (but NOT b),
760+ // and now gopls observes this graph:
761+ //
762+ // a_test
763+ // / \
764+ // b' c'
765+ // /| |
766+ // / d d'
767+ // a-----/
768+ //
769+ // That is: a_test now sees c', which sees d', but since b was not
770+ // invalidated, gopls still thinks that b' imports d (not d')!
771+ //
772+ // The problem, of course, is that gopls never observed the change
773+ // to d, which would have invalidated b. This may be due to racing
774+ // file watching events, in which case the problem should
775+ // self-correct when gopls sees the change to d, or it may be due
776+ // to d being outside the coverage of gopls' file watching glob
777+ // patterns, or it may be due to buggy or entirely absent
778+ // client-side file watching.
779+ //
780+ // TODO(rfindley): fix this, one way or another. It would be hard
781+ // or impossible to repair gopls' state here, during type checking.
782+ // However, we could perhaps reload metadata in Snapshot.load until
783+ // we achieve a consistent state, or better, until the loaded state
784+ // is consistent with our view of the filesystem, by making the Go
785+ // command report digests of the files it reads. Both of those are
786+ // tricker than they may seem, and have significant performance
787+ // implications.
705788 default :
706789 bug .Reportf ("inconsistent view of dependencies" )
707790 }
@@ -723,16 +806,16 @@ func importLookup(mp *metadata.Package, source metadata.Source) func(PackagePath
723806 return id , found
724807 }
725808
726- return func (pkgPath PackagePath ) PackageID {
809+ return func (pkgPath PackagePath ) ( id , altID PackageID ) {
727810 if id , ok := impMap [pkgPath ]; ok {
728- return id
811+ return id , altIDs [ pkgPath ]
729812 }
730813 for len (pending ) > 0 {
731814 if id , found := search (pkgPath ); found {
732- return id
815+ return id , altIDs [ pkgPath ]
733816 }
734817 }
735- return ""
818+ return "" , ""
736819 }
737820}
738821
0 commit comments