Skip to content

Commit f5357f5

Browse files
findleyrgopherbot
authored andcommitted
[gopls-release-branch.0.15] gopls/internal/cache: fix spurious diagnostics in multi-root workspaces
In golang/go#66145, users reported spurious import errors in multi-root workspaces. The problem was related to scenarios where module A had a local replace of module B, and the user opened a file F in module B that wasn't in the forward dependencies of module A. In this case, if the View of module A tried to load F, it would get real packages (not command-line-arguments), but due to module graph pruning the View of module A would not have access to the full set of dependencies for module B, resulting in the potential for import errors. Even this would not be a problem, as long as the package that module A loaded for F was not considered a 'workspace' package. Unfortunately a couple of incorrect heuristics in gopls added along with the zero-config work of [email protected] allowed us to diagnose these broken packages: 1. In resolveImportGraph, we called MetadataForFile for each overlay. As a result, the import graph optimization caused gopls to attempt loading packages for each open file, for each View. It was wrong for an optimization to have this side effect. 2. In golang/go#65801, we observed that it was inconsistent to diagnose changed packages independent of whether they're workspace packages. To fix that, I made all open packages workspace packages. It was probably wrong for the set of workspace packages to depend on open files. To summarize a rather long philosophical digression: open files should determine Views, not packages. Fixing either one of these incorrect heuristics would have prevented golang/go#66145. In this CL, we fix (2) by building the import graph based on existing metadata, without triggering an additional load. For (1), we check IsWorkspacePackage in diagnoseChangedFiles to enforce consistency in the set of diagnosed packages. It would be nice to also remove the heuristic that "all open packages are workspace packages", but we can't do that yet as it would mean no diagnostics for files outside the workspace, after e.g. jumping to definition. A TODO is left to address this another day, when we can be less conservative. Fixes golang/go#66145 Change-Id: Ic4cf2bbbb515b6ea0df24b8e6e46c725b82b4779 Reviewed-on: https://go-review.googlesource.com/c/tools/+/569836 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]> (cherry picked from commit 93c0ca5) Reviewed-on: https://go-review.googlesource.com/c/tools/+/569876 Auto-Submit: Robert Findley <[email protected]>
1 parent 03e6d41 commit f5357f5

File tree

4 files changed

+120
-9
lines changed

4 files changed

+120
-9
lines changed

gopls/internal/cache/check.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,17 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) {
204204

205205
openPackages := make(map[PackageID]bool)
206206
for _, fh := range s.Overlays() {
207-
mps, err := s.MetadataForFile(ctx, fh.URI())
208-
if err != nil {
209-
return nil, err
207+
// golang/go#66145: don't call MetadataForFile here. This function, which
208+
// builds a shared import graph, is an optimization. We don't want it to
209+
// have the side effect of triggering a load.
210+
//
211+
// In the past, a call to MetadataForFile here caused a bunch of
212+
// unnecessary loads in multi-root workspaces (and as a result, spurious
213+
// diagnostics).
214+
g := s.MetadataGraph()
215+
var mps []*metadata.Package
216+
for _, id := range g.IDs[fh.URI()] {
217+
mps = append(mps, g.Packages[id])
210218
}
211219
metadata.RemoveIntermediateTestVariants(&mps)
212220
for _, mp := range mps {

gopls/internal/cache/load.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,22 @@ func computeLoadDiagnostics(ctx context.Context, snapshot *Snapshot, mp *metadat
564564
return diags
565565
}
566566

567+
// IsWorkspacePackage reports whether id points to a workspace package in s.
568+
//
569+
// Currently, the result depends on the current set of loaded packages, and so
570+
// is not guaranteed to be stable.
571+
func (s *Snapshot) IsWorkspacePackage(ctx context.Context, id PackageID) bool {
572+
s.mu.Lock()
573+
defer s.mu.Unlock()
574+
575+
mg := s.meta
576+
m := mg.Packages[id]
577+
if m == nil {
578+
return false
579+
}
580+
return isWorkspacePackageLocked(ctx, s, mg, m)
581+
}
582+
567583
// isWorkspacePackageLocked reports whether p is a workspace package for the
568584
// snapshot s.
569585
//
@@ -575,6 +591,12 @@ func computeLoadDiagnostics(ctx context.Context, snapshot *Snapshot, mp *metadat
575591
// heuristics.
576592
//
577593
// s.mu must be held while calling this function.
594+
//
595+
// TODO(rfindley): remove 'meta' from this function signature. Whether or not a
596+
// package is a workspace package should depend only on the package, view
597+
// definition, and snapshot file source. While useful, the heuristic
598+
// "allFilesHaveRealPackages" does not add that much value and is path
599+
// dependent as it depends on the timing of loads.
578600
func isWorkspacePackageLocked(ctx context.Context, s *Snapshot, meta *metadata.Graph, pkg *metadata.Package) bool {
579601
if metadata.IsCommandLineArguments(pkg.ID) {
580602
// Ad-hoc command-line-arguments packages aren't workspace packages.
@@ -599,12 +621,13 @@ func isWorkspacePackageLocked(ctx context.Context, s *Snapshot, meta *metadata.G
599621
return containsOpenFileLocked(s, pkg)
600622
}
601623

602-
// golang/go#65801: any (non command-line-arguments) open package is a
603-
// workspace package.
624+
// If a real package is open, consider it to be part of the workspace.
604625
//
605-
// Otherwise, we'd display diagnostics for changes in an open package (due to
606-
// the logic of diagnoseChangedFiles), but then erase those diagnostics when
607-
// we do the final diagnostics pass. Diagnostics should be stable.
626+
// TODO(rfindley): reconsider this. In golang/go#66145, we saw that even if a
627+
// View sees a real package for a file, it doesn't mean that View is able to
628+
// cleanly diagnose the package. Yet, we do want to show diagnostics for open
629+
// packages outside the workspace. Is there a better way to ensure that only
630+
// the 'best' View gets a workspace package for the open file?
608631
if containsOpenFileLocked(s, pkg) {
609632
return true
610633
}

gopls/internal/server/diagnostics.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,12 @@ func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snaps
276276
// noisy to log (and we'll handle things later in the slow pass).
277277
continue
278278
}
279-
toDiagnose[meta.ID] = meta
279+
// golang/go#65801: only diagnose changes to workspace packages. Otherwise,
280+
// diagnostics will be unstable, as the slow-path diagnostics will erase
281+
// them.
282+
if snapshot.IsWorkspacePackage(ctx, meta.ID) {
283+
toDiagnose[meta.ID] = meta
284+
}
280285
}
281286
diags, err := snapshot.PackageDiagnostics(ctx, maps.Keys(toDiagnose)...)
282287
if err != nil {

gopls/internal/test/integration/workspace/multi_folder_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,78 @@ func _() {
5151
)
5252
})
5353
}
54+
55+
func TestMultiView_LocalReplace(t *testing.T) {
56+
// This is a regression test for #66145, where gopls attempted to load a
57+
// package in a locally replaced module as a workspace package, resulting in
58+
// spurious import diagnostics because the module graph had been pruned.
59+
60+
const proxy = `
61+
-- example.com/[email protected]/go.mod --
62+
module example.com/c
63+
64+
go 1.20
65+
66+
-- example.com/[email protected]/c.go --
67+
package c
68+
69+
const C = 3
70+
71+
`
72+
// In the past, gopls would only diagnose one View at a time
73+
// (the last to have changed).
74+
//
75+
// This test verifies that gopls can maintain diagnostics for multiple Views.
76+
const files = `
77+
-- a/go.mod --
78+
module golang.org/lsptests/a
79+
80+
go 1.20
81+
82+
require golang.org/lsptests/b v1.2.3
83+
84+
replace golang.org/lsptests/b => ../b
85+
86+
-- a/a.go --
87+
package a
88+
89+
import "golang.org/lsptests/b"
90+
91+
const A = b.B - 1
92+
93+
-- b/go.mod --
94+
module golang.org/lsptests/b
95+
96+
go 1.20
97+
98+
require example.com/c v1.2.3
99+
100+
-- b/go.sum --
101+
example.com/c v1.2.3 h1:hsOPhoHQLZPEn7l3kNya3fR3SfqW0/rafZMP8ave6fg=
102+
example.com/c v1.2.3/go.mod h1:4uG6Y5qX88LrEd4KfRoiguHZIbdLKUEHD1wXqPyrHcA=
103+
-- b/b.go --
104+
package b
105+
106+
const B = 2
107+
108+
-- b/unrelated/u.go --
109+
package unrelated
110+
111+
import "example.com/c"
112+
113+
const U = c.C
114+
`
115+
116+
WithOptions(
117+
WorkspaceFolders("a", "b"),
118+
ProxyFiles(proxy),
119+
).Run(t, files, func(t *testing.T, env *Env) {
120+
// Opening unrelated first ensures that when we compute workspace packages
121+
// for the "a" workspace, it includes the unrelated package, which will be
122+
// unloadable from a as there is no a/go.sum.
123+
env.OpenFile("b/unrelated/u.go")
124+
env.AfterChange()
125+
env.OpenFile("a/a.go")
126+
env.AfterChange(NoDiagnostics())
127+
})
128+
}

0 commit comments

Comments
 (0)