Skip to content

Commit 9619683

Browse files
committed
gopls/internal/cache: treat local replaces as workspace modules
Update the view selection algorithm to consider replace directives, treating locally replaced modules in a go.mod file as workspace modules. This causes gopls to register watch patterns for these local replaces. Since this is a fundamental change in behavior, add a hidden off switch to revert to the old behavior: an "includeReplaceInWorkspace" setting. Fixes golang/go#64762 Fixes golang/go#64888 Change-Id: I0fea97a05299acd877a220982d51ba9b4d4070e3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/562680 Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent a5af84e commit 9619683

File tree

6 files changed

+214
-13
lines changed

6 files changed

+214
-13
lines changed

gopls/internal/cache/session_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,91 @@ func TestZeroConfigAlgorithm(t *testing.T) {
227227
[]string{"a/a.go", "b/b.go", "b/c/c.go"},
228228
[]viewSummary{{GoWorkView, ".", nil}, {GoModView, "b/c", []string{"GOWORK=off"}}},
229229
},
230+
{
231+
"go.mod with nested replace",
232+
map[string]string{
233+
"go.mod": "module golang.org/a\n require golang.org/b v1.2.3\nreplace example.com/b => ./b",
234+
"a.go": "package a",
235+
"b/go.mod": "module golang.org/b\ngo 1.18\n",
236+
"b/b.go": "package b",
237+
},
238+
[]folderSummary{{dir: "."}},
239+
[]string{"a/a.go", "b/b.go"},
240+
[]viewSummary{{GoModView, ".", nil}},
241+
},
242+
{
243+
"go.mod with parent replace, parent folder",
244+
map[string]string{
245+
"go.mod": "module golang.org/a",
246+
"a.go": "package a",
247+
"b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../",
248+
"b/b.go": "package b",
249+
},
250+
[]folderSummary{{dir: "."}},
251+
[]string{"a/a.go", "b/b.go"},
252+
[]viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}},
253+
},
254+
{
255+
"go.mod with multiple replace",
256+
map[string]string{
257+
"go.mod": `
258+
module golang.org/root
259+
260+
require (
261+
golang.org/a v1.2.3
262+
golang.org/b v1.2.3
263+
golang.org/c v1.2.3
264+
)
265+
266+
replace (
267+
golang.org/b => ./b
268+
golang.org/c => ./c
269+
// Note: d is not replaced
270+
)
271+
`,
272+
"a.go": "package a",
273+
"b/go.mod": "module golang.org/b\ngo 1.18",
274+
"b/b.go": "package b",
275+
"c/go.mod": "module golang.org/c\ngo 1.18",
276+
"c/c.go": "package c",
277+
"d/go.mod": "module golang.org/d\ngo 1.18",
278+
"d/d.go": "package d",
279+
},
280+
[]folderSummary{{dir: "."}},
281+
[]string{"b/b.go", "c/c.go", "d/d.go"},
282+
[]viewSummary{{GoModView, ".", nil}, {GoModView, "d", nil}},
283+
},
284+
{
285+
"go.mod with many replace",
286+
map[string]string{
287+
"go.mod": "module golang.org/a\ngo 1.18",
288+
"a.go": "package a",
289+
"b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../",
290+
"b/b.go": "package b",
291+
},
292+
[]folderSummary{{dir: "b"}},
293+
[]string{"a/a.go", "b/b.go"},
294+
[]viewSummary{{GoModView, "b", nil}},
295+
},
296+
{
297+
"go.mod with replace directive; workspace replace off",
298+
map[string]string{
299+
"go.mod": "module golang.org/a\n require golang.org/b v1.2.3\nreplace example.com/b => ./b",
300+
"a.go": "package a",
301+
"b/go.mod": "module golang.org/b\ngo 1.18\n",
302+
"b/b.go": "package b",
303+
},
304+
[]folderSummary{{
305+
dir: ".",
306+
options: func(string) map[string]any {
307+
return map[string]any{
308+
"includeReplaceInWorkspace": false,
309+
}
310+
},
311+
}},
312+
[]string{"a/a.go", "b/b.go"},
313+
[]viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}},
314+
},
230315
}
231316

232317
for _, test := range tests {

gopls/internal/cache/view.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,10 @@ type viewDefinition struct {
154154

155155
// workspaceModFiles holds the set of mod files active in this snapshot.
156156
//
157-
// This is either empty, a single entry for the workspace go.mod file, or the
158-
// set of mod files used by the workspace go.work file.
157+
// For a go.work workspace, this is the set of workspace modfiles. For a
158+
// go.mod workspace, this contains the go.mod file defining the workspace
159+
// root, as well as any locally replaced modules (if
160+
// "includeReplaceInWorkspace" is set).
159161
//
160162
// TODO(rfindley): should we just run `go list -m` to compute this set?
161163
workspaceModFiles map[protocol.DocumentURI]struct{}
@@ -939,6 +941,22 @@ func defineView(ctx context.Context, fs file.Source, folder *Folder, forFile fil
939941
// But gopls is less strict, allowing GOPATH mode if GO111MODULE="", and
940942
// AdHoc views if no module is found.
941943

944+
// gomodWorkspace is a helper to compute the correct set of workspace
945+
// modfiles for a go.mod file, based on folder options.
946+
gomodWorkspace := func() map[protocol.DocumentURI]unit {
947+
modFiles := map[protocol.DocumentURI]struct{}{def.gomod: {}}
948+
if folder.Options.IncludeReplaceInWorkspace {
949+
includingReplace, err := goModModules(ctx, def.gomod, fs)
950+
if err == nil {
951+
modFiles = includingReplace
952+
} else {
953+
// If the go.mod file fails to parse, we don't know anything about
954+
// replace directives, so fall back to a view of just the root module.
955+
}
956+
}
957+
return modFiles
958+
}
959+
942960
// Prefer a go.work file if it is available and contains the module relevant
943961
// to forURI.
944962
if def.adjustedGO111MODULE() != "off" && folder.Env.GOWORK != "off" && def.gowork != "" {
@@ -959,7 +977,7 @@ func defineView(ctx context.Context, fs file.Source, folder *Folder, forFile fil
959977
if _, ok := def.workspaceModFiles[def.gomod]; !ok {
960978
def.typ = GoModView
961979
def.root = def.gomod.Dir()
962-
def.workspaceModFiles = map[protocol.DocumentURI]unit{def.gomod: {}}
980+
def.workspaceModFiles = gomodWorkspace()
963981
if def.envOverlay == nil {
964982
def.envOverlay = make(map[string]string)
965983
}
@@ -978,7 +996,7 @@ func defineView(ctx context.Context, fs file.Source, folder *Folder, forFile fil
978996
if def.adjustedGO111MODULE() != "off" && def.gomod != "" {
979997
def.typ = GoModView
980998
def.root = def.gomod.Dir()
981-
def.workspaceModFiles = map[protocol.DocumentURI]struct{}{def.gomod: {}}
999+
def.workspaceModFiles = gomodWorkspace()
9821000
return def, nil
9831001
}
9841002

gopls/internal/cache/workspace.go

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import (
1515
"golang.org/x/tools/gopls/internal/protocol"
1616
)
1717

18-
// TODO(rfindley): now that experimentalWorkspaceModule is gone, this file can
19-
// be massively cleaned up and/or removed.
18+
// isGoWork reports if uri is a go.work file.
19+
func isGoWork(uri protocol.DocumentURI) bool {
20+
return filepath.Base(uri.Path()) == "go.work"
21+
}
2022

2123
// goWorkModules returns the URIs of go.mod files named by the go.work file.
2224
func goWorkModules(ctx context.Context, gowork protocol.DocumentURI, fs file.Source) (map[protocol.DocumentURI]unit, error) {
@@ -34,26 +36,62 @@ func goWorkModules(ctx context.Context, gowork protocol.DocumentURI, fs file.Sou
3436
if err != nil {
3537
return nil, fmt.Errorf("parsing go.work: %w", err)
3638
}
37-
modFiles := make(map[protocol.DocumentURI]unit)
39+
var usedDirs []string
3840
for _, use := range workFile.Use {
39-
modDir := filepath.FromSlash(use.Path)
41+
usedDirs = append(usedDirs, use.Path)
42+
}
43+
return localModFiles(dir, usedDirs), nil
44+
}
45+
46+
// localModFiles builds a set of local go.mod files referenced by
47+
// goWorkOrModPaths, which is a slice of paths as contained in a go.work 'use'
48+
// directive or go.mod 'replace' directive (and which therefore may use either
49+
// '/' or '\' as a path separator).
50+
func localModFiles(relativeTo string, goWorkOrModPaths []string) map[protocol.DocumentURI]unit {
51+
modFiles := make(map[protocol.DocumentURI]unit)
52+
for _, path := range goWorkOrModPaths {
53+
modDir := filepath.FromSlash(path)
4054
if !filepath.IsAbs(modDir) {
41-
modDir = filepath.Join(dir, modDir)
55+
modDir = filepath.Join(relativeTo, modDir)
4256
}
4357
modURI := protocol.URIFromPath(filepath.Join(modDir, "go.mod"))
4458
modFiles[modURI] = unit{}
4559
}
46-
return modFiles, nil
60+
return modFiles
4761
}
4862

4963
// isGoMod reports if uri is a go.mod file.
5064
func isGoMod(uri protocol.DocumentURI) bool {
5165
return filepath.Base(uri.Path()) == "go.mod"
5266
}
5367

54-
// isGoWork reports if uri is a go.work file.
55-
func isGoWork(uri protocol.DocumentURI) bool {
56-
return filepath.Base(uri.Path()) == "go.work"
68+
// goModModules returns the URIs of "workspace" go.mod files defined by a
69+
// go.mod file. This set is defined to be the given go.mod file itself, as well
70+
// as the modfiles of any locally replaced modules in the go.mod file.
71+
func goModModules(ctx context.Context, gomod protocol.DocumentURI, fs file.Source) (map[protocol.DocumentURI]unit, error) {
72+
fh, err := fs.ReadFile(ctx, gomod)
73+
if err != nil {
74+
return nil, err // canceled
75+
}
76+
content, err := fh.Content()
77+
if err != nil {
78+
return nil, err
79+
}
80+
filename := gomod.Path()
81+
dir := filepath.Dir(filename)
82+
modFile, err := modfile.Parse(filename, content, nil)
83+
if err != nil {
84+
return nil, err
85+
}
86+
var localReplaces []string
87+
for _, replace := range modFile.Replace {
88+
if modfile.IsDirectoryPath(replace.New.Path) {
89+
localReplaces = append(localReplaces, replace.New.Path)
90+
}
91+
}
92+
modFiles := localModFiles(dir, localReplaces)
93+
modFiles[gomod] = unit{}
94+
return modFiles, nil
5795
}
5896

5997
// fileExists reports whether the file has a Content (which may be empty).

gopls/internal/settings/default.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func DefaultOptions(overrides ...func(*Options)) *Options {
115115
ReportAnalysisProgressAfter: 5 * time.Second,
116116
TelemetryPrompt: false,
117117
LinkifyShowMessage: false,
118+
IncludeReplaceInWorkspace: true,
118119
},
119120
Hooks: Hooks{
120121
URLRegexp: urlRegexp(),

gopls/internal/settings/settings.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,12 @@ type InternalOptions struct {
553553
// LinkifyShowMessage controls whether the client wants gopls
554554
// to linkify links in showMessage. e.g. [go.dev](https://go.dev).
555555
LinkifyShowMessage bool
556+
557+
// IncludeReplaceInWorkspace controls whether locally replaced modules in a
558+
// go.mod file are treated like workspace modules.
559+
// Or in other words, if a go.mod file with local replaces behaves like a
560+
// go.work file.
561+
IncludeReplaceInWorkspace bool
556562
}
557563

558564
type SubdirWatchPatterns string
@@ -1146,9 +1152,13 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{})
11461152

11471153
case "telemetryPrompt":
11481154
result.setBool(&o.TelemetryPrompt)
1155+
11491156
case "linkifyShowMessage":
11501157
result.setBool(&o.LinkifyShowMessage)
11511158

1159+
case "includeReplaceInWorkspace":
1160+
result.setBool(&o.IncludeReplaceInWorkspace)
1161+
11521162
// Replaced settings.
11531163
case "experimentalDisabledAnalyses":
11541164
result.deprecated("analyses")

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,52 @@ const C = 0
185185
)
186186
})
187187
}
188+
189+
func TestGoModReplace(t *testing.T) {
190+
// This test checks that we treat locally replaced modules as workspace
191+
// modules, according to the "includeReplaceInWorkspace" setting.
192+
const files = `
193+
-- moda/go.mod --
194+
module golang.org/a
195+
196+
require golang.org/b v1.2.3
197+
198+
replace golang.org/b => ../modb
199+
200+
go 1.20
201+
202+
-- moda/a.go --
203+
package a
204+
205+
import "golang.org/b"
206+
207+
const A = b.B
208+
209+
-- modb/go.mod --
210+
module golang.org/b
211+
212+
go 1.20
213+
214+
-- modb/b.go --
215+
package b
216+
217+
const B = 1
218+
`
219+
220+
for useReplace, expectation := range map[bool]Expectation{
221+
true: FileWatchMatching("modb"),
222+
false: NoFileWatchMatching("modb"),
223+
} {
224+
WithOptions(
225+
WorkspaceFolders("moda"),
226+
Settings{
227+
"includeReplaceInWorkspace": useReplace,
228+
},
229+
).Run(t, files, func(t *testing.T, env *Env) {
230+
env.OnceMet(
231+
InitialWorkspaceLoad,
232+
expectation,
233+
)
234+
})
235+
}
236+
}

0 commit comments

Comments
 (0)