Skip to content

Commit e5e8aa8

Browse files
firelizzard18gopherbot
authored andcommitted
gopls/internal: implement Modules command
Implements the `gopls.modules` command. Updates golang/go#59445 Change-Id: Ifb39e0ba79be688af81ddc6389570011b4d441cc Reviewed-on: https://go-review.googlesource.com/c/tools/+/598815 Reviewed-by: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 9ef0547 commit e5e8aa8

File tree

5 files changed

+257
-7
lines changed

5 files changed

+257
-7
lines changed

gopls/internal/protocol/command/interface.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,12 @@ type Interface interface {
282282

283283
// Modules: Return information about modules within a directory
284284
//
285-
// This command returns an empty result if there is no module,
286-
// or if module mode is disabled.
287-
// The result does not includes the modules that are not
288-
// associated with any Views on the server yet.
285+
// This command returns an empty result if there is no module, or if module
286+
// mode is disabled. Modules will not cause any new views to be loaded and
287+
// will only return modules associated with views that have already been
288+
// loaded, regardless of how it is called. Given current usage (by the
289+
// language server client), there should never be a case where Modules is
290+
// called on a path that has not already been loaded.
289291
Modules(context.Context, ModulesArgs) (ModulesResult, error)
290292
}
291293

gopls/internal/server/command.go

+70-2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,76 @@ type commandHandler struct {
7171
params *protocol.ExecuteCommandParams
7272
}
7373

74-
func (h *commandHandler) Modules(context.Context, command.ModulesArgs) (command.ModulesResult, error) {
75-
panic("unimplemented")
74+
func (h *commandHandler) Modules(ctx context.Context, args command.ModulesArgs) (command.ModulesResult, error) {
75+
// keepModule filters modules based on the command args
76+
keepModule := func(goMod protocol.DocumentURI) bool {
77+
// Does the directory enclose the view's go.mod file?
78+
if !args.Dir.Encloses(goMod) {
79+
return false
80+
}
81+
82+
// Calculate the relative path
83+
rel, err := filepath.Rel(args.Dir.Path(), goMod.Path())
84+
if err != nil {
85+
return false // "can't happen" (see prior Encloses check)
86+
}
87+
88+
assert(filepath.Base(goMod.Path()) == "go.mod", fmt.Sprintf("invalid go.mod path: want go.mod, got %q", goMod.Path()))
89+
90+
// Invariant: rel is a relative path without "../" segments and the last
91+
// segment is "go.mod"
92+
nparts := strings.Count(rel, string(filepath.Separator))
93+
return args.MaxDepth < 0 || nparts <= args.MaxDepth
94+
}
95+
96+
// Views may include:
97+
// - go.work views containing one or more modules each;
98+
// - go.mod views containing a single module each;
99+
// - GOPATH and/or ad hoc views containing no modules.
100+
//
101+
// Retrieving a view via the request path would only work for a
102+
// non-recursive query for a go.mod view, and even in that case
103+
// [Session.SnapshotOf] doesn't work on directories. Thus we check every
104+
// view.
105+
var result command.ModulesResult
106+
seen := map[protocol.DocumentURI]bool{}
107+
for _, v := range h.s.session.Views() {
108+
s, release, err := v.Snapshot()
109+
if err != nil {
110+
return command.ModulesResult{}, err
111+
}
112+
defer release()
113+
114+
for _, modFile := range v.ModFiles() {
115+
if !keepModule(modFile) {
116+
continue
117+
}
118+
119+
// Deduplicate
120+
if seen[modFile] {
121+
continue
122+
}
123+
seen[modFile] = true
124+
125+
fh, err := s.ReadFile(ctx, modFile)
126+
if err != nil {
127+
return command.ModulesResult{}, err
128+
}
129+
mod, err := s.ParseMod(ctx, fh)
130+
if err != nil {
131+
return command.ModulesResult{}, err
132+
}
133+
if mod.File.Module == nil {
134+
continue // syntax contains errors
135+
}
136+
result.Modules = append(result.Modules, command.Module{
137+
Path: mod.File.Module.Mod.Path,
138+
Version: mod.File.Module.Mod.Version,
139+
GoMod: mod.URI,
140+
})
141+
}
142+
}
143+
return result, nil
76144
}
77145

78146
func (h *commandHandler) Packages(context.Context, command.PackagesArgs) (command.PackagesResult, error) {

gopls/internal/server/debug.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package server
6+
7+
// assert panics with the given msg if cond is not true.
8+
func assert(cond bool, msg string) {
9+
if !cond {
10+
panic(msg)
11+
}
12+
}

gopls/internal/test/integration/fake/editor.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -633,9 +633,13 @@ func (e *Editor) sendDidClose(ctx context.Context, doc protocol.TextDocumentIden
633633
return nil
634634
}
635635

636+
func (e *Editor) DocumentURI(path string) protocol.DocumentURI {
637+
return e.sandbox.Workdir.URI(path)
638+
}
639+
636640
func (e *Editor) TextDocumentIdentifier(path string) protocol.TextDocumentIdentifier {
637641
return protocol.TextDocumentIdentifier{
638-
URI: e.sandbox.Workdir.URI(path),
642+
URI: e.DocumentURI(path),
639643
}
640644
}
641645

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package workspace
6+
7+
import (
8+
"sort"
9+
"strings"
10+
"testing"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"golang.org/x/tools/gopls/internal/protocol"
14+
"golang.org/x/tools/gopls/internal/protocol/command"
15+
. "golang.org/x/tools/gopls/internal/test/integration"
16+
)
17+
18+
func TestModulesCmd(t *testing.T) {
19+
const goModView = `
20+
-- go.mod --
21+
module foo
22+
23+
-- pkg/pkg.go --
24+
package pkg
25+
func Pkg()
26+
27+
-- bar/bar.go --
28+
package bar
29+
func Bar()
30+
31+
-- bar/baz/go.mod --
32+
module baz
33+
34+
-- bar/baz/baz.go --
35+
package baz
36+
func Baz()
37+
`
38+
39+
const goWorkView = `
40+
-- go.work --
41+
use ./foo
42+
use ./bar
43+
44+
-- foo/go.mod --
45+
module foo
46+
47+
-- foo/foo.go --
48+
package foo
49+
func Foo()
50+
51+
-- bar/go.mod --
52+
module bar
53+
54+
-- bar/bar.go --
55+
package bar
56+
func Bar()
57+
`
58+
59+
t.Run("go.mod view", func(t *testing.T) {
60+
// If baz isn't loaded, it will not be included
61+
t.Run("unloaded", func(t *testing.T) {
62+
Run(t, goModView, func(t *testing.T, env *Env) {
63+
checkModules(t, env, env.Editor.DocumentURI(""), -1, []command.Module{
64+
{
65+
Path: "foo",
66+
GoMod: env.Editor.DocumentURI("go.mod"),
67+
},
68+
})
69+
})
70+
})
71+
72+
// With baz loaded and recursion enabled, baz will be included
73+
t.Run("recurse", func(t *testing.T) {
74+
Run(t, goModView, func(t *testing.T, env *Env) {
75+
env.OpenFile("bar/baz/baz.go")
76+
checkModules(t, env, env.Editor.DocumentURI(""), -1, []command.Module{
77+
{
78+
Path: "baz",
79+
GoMod: env.Editor.DocumentURI("bar/baz/go.mod"),
80+
},
81+
{
82+
Path: "foo",
83+
GoMod: env.Editor.DocumentURI("go.mod"),
84+
},
85+
})
86+
})
87+
})
88+
89+
// With recursion=1, baz will not be included
90+
t.Run("depth", func(t *testing.T) {
91+
Run(t, goModView, func(t *testing.T, env *Env) {
92+
env.OpenFile("bar/baz/baz.go")
93+
checkModules(t, env, env.Editor.DocumentURI(""), 1, []command.Module{
94+
{
95+
Path: "foo",
96+
GoMod: env.Editor.DocumentURI("go.mod"),
97+
},
98+
})
99+
})
100+
})
101+
102+
// Baz will be included if it is requested specifically
103+
t.Run("nested", func(t *testing.T) {
104+
Run(t, goModView, func(t *testing.T, env *Env) {
105+
env.OpenFile("bar/baz/baz.go")
106+
checkModules(t, env, env.Editor.DocumentURI("bar/baz"), 0, []command.Module{
107+
{
108+
Path: "baz",
109+
GoMod: env.Editor.DocumentURI("bar/baz/go.mod"),
110+
},
111+
})
112+
})
113+
})
114+
})
115+
116+
t.Run("go.work view", func(t *testing.T) {
117+
t.Run("base", func(t *testing.T) {
118+
Run(t, goWorkView, func(t *testing.T, env *Env) {
119+
checkModules(t, env, env.Editor.DocumentURI(""), 0, nil)
120+
})
121+
})
122+
123+
t.Run("recursive", func(t *testing.T) {
124+
Run(t, goWorkView, func(t *testing.T, env *Env) {
125+
checkModules(t, env, env.Editor.DocumentURI(""), -1, []command.Module{
126+
{
127+
Path: "bar",
128+
GoMod: env.Editor.DocumentURI("bar/go.mod"),
129+
},
130+
{
131+
Path: "foo",
132+
GoMod: env.Editor.DocumentURI("foo/go.mod"),
133+
},
134+
})
135+
})
136+
})
137+
})
138+
}
139+
140+
func checkModules(t testing.TB, env *Env, dir protocol.DocumentURI, maxDepth int, want []command.Module) {
141+
t.Helper()
142+
143+
cmd, err := command.NewModulesCommand("Modules", command.ModulesArgs{Dir: dir, MaxDepth: maxDepth})
144+
if err != nil {
145+
t.Fatal(err)
146+
}
147+
var result command.ModulesResult
148+
env.ExecuteCommand(&protocol.ExecuteCommandParams{
149+
Command: command.Modules.String(),
150+
Arguments: cmd.Arguments,
151+
}, &result)
152+
153+
// The ordering of results is undefined and modules from a go.work view are
154+
// retrieved from a map, so sort the results to ensure consistency
155+
sort.Slice(result.Modules, func(i, j int) bool {
156+
a, b := result.Modules[i], result.Modules[j]
157+
return strings.Compare(a.Path, b.Path) < 0
158+
})
159+
160+
diff := cmp.Diff(want, result.Modules)
161+
if diff != "" {
162+
t.Errorf("Modules(%v) returned unexpected diff (-want +got):\n%s", dir, diff)
163+
}
164+
}

0 commit comments

Comments
 (0)