Skip to content

Commit 906c733

Browse files
committed
gopls/internal/lsp/source: find references in test packages
When finding references or implementations, we must include objects defined in intermediate test variants. However, as we have seen we should be careful to avoid accidentally type-checking intermediate test variants in ParseFull, which is not their workspace parse mode. Therefore eliminate the problematic TypecheckAll type-check mode in favor of special handling in this one case where it is necessary. Along the way: - Simplify the mapping of protocol position->offset. This should not require getting a package, or even parsing a file. For isolation, just use the NewColumnMapper constructor, even though it may technically result in building a token.File multiple times. - Update package renaming logic to use TypecheckWorkspace, since we only need to rename within the workspace. - Add regtest support for Implementations requests. Fixes golang/go#43144 Change-Id: I41f684ad766f5af805abbd7c5ee0a010ff9b9b8c Reviewed-on: https://go-review.googlesource.com/c/tools/+/438755 gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 2a41b25 commit 906c733

File tree

9 files changed

+208
-55
lines changed

9 files changed

+208
-55
lines changed

gopls/internal/lsp/cache/load.go

+1
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[Packag
655655
if !m.Valid {
656656
continue
657657
}
658+
658659
if !containsPackageLocked(s, m.Metadata) {
659660
continue
660661
}

gopls/internal/lsp/cache/snapshot.go

+7-18
Original file line numberDiff line numberDiff line change
@@ -698,27 +698,16 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode
698698
if m := s.getMetadata(id); m != nil && m.IsIntermediateTestVariant() && !withIntermediateTestVariants {
699699
continue
700700
}
701-
var parseModes []source.ParseMode
702-
switch mode {
703-
case source.TypecheckAll:
704-
if s.workspaceParseMode(id) == source.ParseFull {
705-
parseModes = []source.ParseMode{source.ParseFull}
706-
} else {
707-
parseModes = []source.ParseMode{source.ParseExported, source.ParseFull}
708-
}
709-
case source.TypecheckFull:
710-
parseModes = []source.ParseMode{source.ParseFull}
711-
case source.TypecheckWorkspace:
712-
parseModes = []source.ParseMode{s.workspaceParseMode(id)}
701+
parseMode := source.ParseFull
702+
if mode == source.TypecheckWorkspace {
703+
parseMode = s.workspaceParseMode(id)
713704
}
714705

715-
for _, parseMode := range parseModes {
716-
ph, err := s.buildPackageHandle(ctx, id, parseMode)
717-
if err != nil {
718-
return nil, err
719-
}
720-
phs = append(phs, ph)
706+
ph, err := s.buildPackageHandle(ctx, id, parseMode)
707+
if err != nil {
708+
return nil, err
721709
}
710+
phs = append(phs, ph)
722711
}
723712
return phs, nil
724713
}

gopls/internal/lsp/fake/editor.go

+26-1
Original file line numberDiff line numberDiff line change
@@ -1137,7 +1137,8 @@ func (e *Editor) InlayHint(ctx context.Context, path string) ([]protocol.InlayHi
11371137
return hints, nil
11381138
}
11391139

1140-
// References executes a reference request on the server.
1140+
// References returns references to the object at (path, pos), as returned by
1141+
// the connected LSP server. If no server is connected, it returns (nil, nil).
11411142
func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) {
11421143
if e.Server == nil {
11431144
return nil, nil
@@ -1164,6 +1165,8 @@ func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protoc
11641165
return locations, nil
11651166
}
11661167

1168+
// Rename performs a rename of the object at (path, pos) to newName, using the
1169+
// connected LSP server. If no server is connected, it returns nil.
11671170
func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName string) error {
11681171
if e.Server == nil {
11691172
return nil
@@ -1185,6 +1188,28 @@ func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName strin
11851188
return nil
11861189
}
11871190

1191+
// Implementations returns implementations for the object at (path, pos), as
1192+
// returned by the connected LSP server. If no server is connected, it returns
1193+
// (nil, nil).
1194+
func (e *Editor) Implementations(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) {
1195+
if e.Server == nil {
1196+
return nil, nil
1197+
}
1198+
e.mu.Lock()
1199+
_, ok := e.buffers[path]
1200+
e.mu.Unlock()
1201+
if !ok {
1202+
return nil, fmt.Errorf("buffer %q is not open", path)
1203+
}
1204+
params := &protocol.ImplementationParams{
1205+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
1206+
TextDocument: e.TextDocumentIdentifier(path),
1207+
Position: pos.ToProtocolPosition(),
1208+
},
1209+
}
1210+
return e.Server.Implementation(ctx, params)
1211+
}
1212+
11881213
func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error {
11891214
closed, opened, err := e.renameBuffers(ctx, oldPath, newPath)
11901215
if err != nil {

gopls/internal/lsp/regtest/wrappers.go

+11-2
Original file line numberDiff line numberDiff line change
@@ -389,8 +389,7 @@ func (e *Env) WorkspaceSymbol(sym string) []protocol.SymbolInformation {
389389
return ans
390390
}
391391

392-
// References calls textDocument/references for the given path at the given
393-
// position.
392+
// References wraps Editor.References, calling t.Fatal on any error.
394393
func (e *Env) References(path string, pos fake.Pos) []protocol.Location {
395394
e.T.Helper()
396395
locations, err := e.Editor.References(e.Ctx, path, pos)
@@ -408,6 +407,16 @@ func (e *Env) Rename(path string, pos fake.Pos, newName string) {
408407
}
409408
}
410409

410+
// Implementations wraps Editor.Implementations, calling t.Fatal on any error.
411+
func (e *Env) Implementations(path string, pos fake.Pos) []protocol.Location {
412+
e.T.Helper()
413+
locations, err := e.Editor.Implementations(e.Ctx, path, pos)
414+
if err != nil {
415+
e.T.Fatal(err)
416+
}
417+
return locations
418+
}
419+
411420
// RenameFile wraps Editor.RenameFile, calling t.Fatal on any error.
412421
func (e *Env) RenameFile(oldPath, newPath string) {
413422
e.T.Helper()

gopls/internal/lsp/source/implementation.go

+40-25
Original file line numberDiff line numberDiff line change
@@ -208,36 +208,31 @@ var (
208208
errNoObjectFound = errors.New("no object found")
209209
)
210210

211-
// qualifiedObjsAtProtocolPos returns info for all the type.Objects
212-
// referenced at the given position. An object will be returned for
213-
// every package that the file belongs to, in every typechecking mode
214-
// applicable.
211+
// qualifiedObjsAtProtocolPos returns info for all the types.Objects referenced
212+
// at the given position, for the following selection of packages:
213+
//
214+
// 1. all packages (including all test variants), in their workspace parse mode
215+
// 2. if not included above, at least one package containing uri in full parse mode
216+
//
217+
// Finding objects in (1) ensures that we locate references within all
218+
// workspace packages, including in x_test packages. Including (2) ensures that
219+
// we find local references in the current package, for non-workspace packages
220+
// that may be open.
215221
func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, uri span.URI, pp protocol.Position) ([]qualifiedObject, error) {
216-
offset, err := protocolPositionToOffset(ctx, s, uri, pp)
222+
fh, err := s.GetFile(ctx, uri)
217223
if err != nil {
218224
return nil, err
219225
}
220-
return qualifiedObjsAtLocation(ctx, s, positionKey{uri, offset}, map[positionKey]bool{})
221-
}
222-
223-
func protocolPositionToOffset(ctx context.Context, s Snapshot, uri span.URI, pp protocol.Position) (int, error) {
224-
pkgs, err := s.PackagesForFile(ctx, uri, TypecheckAll, false)
226+
content, err := fh.Read()
225227
if err != nil {
226-
return 0, err
227-
}
228-
if len(pkgs) == 0 {
229-
return 0, errNoObjectFound
230-
}
231-
pkg := pkgs[0]
232-
pgf, err := pkg.File(uri)
233-
if err != nil {
234-
return 0, err
228+
return nil, err
235229
}
236-
pos, err := pgf.Mapper.Pos(pp)
230+
m := protocol.NewColumnMapper(uri, content)
231+
offset, err := m.Offset(pp)
237232
if err != nil {
238-
return 0, err
233+
return nil, err
239234
}
240-
return safetoken.Offset(pgf.Tok, pos)
235+
return qualifiedObjsAtLocation(ctx, s, positionKey{uri, offset}, map[positionKey]bool{})
241236
}
242237

243238
// A positionKey identifies a byte offset within a file (URI).
@@ -251,8 +246,8 @@ type positionKey struct {
251246
offset int
252247
}
253248

254-
// qualifiedObjsAtLocation finds all objects referenced at offset in uri, across
255-
// all packages in the snapshot.
249+
// qualifiedObjsAtLocation finds all objects referenced at offset in uri,
250+
// across all packages in the snapshot.
256251
func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, seen map[positionKey]bool) ([]qualifiedObject, error) {
257252
if seen[key] {
258253
return nil, nil
@@ -268,11 +263,31 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s
268263
// try to be comprehensive in case we ever support variations on build
269264
// constraints.
270265

271-
pkgs, err := s.PackagesForFile(ctx, key.uri, TypecheckAll, false)
266+
pkgs, err := s.PackagesForFile(ctx, key.uri, TypecheckWorkspace, true)
272267
if err != nil {
273268
return nil, err
274269
}
275270

271+
// In order to allow basic references/rename/implementations to function when
272+
// non-workspace packages are open, ensure that we have at least one fully
273+
// parsed package for the current file. This allows us to find references
274+
// inside the open package. Use WidestPackage to capture references in test
275+
// files.
276+
hasFullPackage := false
277+
for _, pkg := range pkgs {
278+
if pkg.ParseMode() == ParseFull {
279+
hasFullPackage = true
280+
break
281+
}
282+
}
283+
if !hasFullPackage {
284+
pkg, err := s.PackageForFile(ctx, key.uri, TypecheckFull, WidestPackage)
285+
if err != nil {
286+
return nil, err
287+
}
288+
pkgs = append(pkgs, pkg)
289+
}
290+
276291
// report objects in the order we encounter them. This ensures that the first
277292
// result is at the cursor...
278293
var qualifiedObjs []qualifiedObject

gopls/internal/lsp/source/references.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit
6262
}
6363

6464
if inPackageName {
65-
renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckAll, NarrowestPackage)
65+
renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckWorkspace, NarrowestPackage)
6666
if err != nil {
6767
return nil, err
6868
}

gopls/internal/lsp/source/rename.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot
7676
err := errors.New("can't rename package: LSP client does not support file renaming")
7777
return nil, err, err
7878
}
79-
renamingPkg, err := snapshot.PackageForFile(ctx, f.URI(), TypecheckAll, NarrowestPackage)
79+
renamingPkg, err := snapshot.PackageForFile(ctx, f.URI(), TypecheckWorkspace, NarrowestPackage)
8080
if err != nil {
8181
return nil, err, err
8282
}
@@ -169,7 +169,7 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position,
169169
// TODO(rfindley): but is this correct? What about x_test packages that
170170
// import the renaming package?
171171
const includeTestVariants = false
172-
pkgs, err := s.PackagesForFile(ctx, f.URI(), TypecheckAll, includeTestVariants)
172+
pkgs, err := s.PackagesForFile(ctx, f.URI(), TypecheckWorkspace, includeTestVariants)
173173
if err != nil {
174174
return nil, true, err
175175
}

gopls/internal/lsp/source/view.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -437,16 +437,11 @@ var AllParseModes = []ParseMode{ParseHeader, ParseExported, ParseFull}
437437
type TypecheckMode int
438438

439439
const (
440-
// Invalid default value.
441-
TypecheckUnknown TypecheckMode = iota
442440
// TypecheckFull means to use ParseFull.
443-
TypecheckFull
441+
TypecheckFull TypecheckMode = iota
444442
// TypecheckWorkspace means to use ParseFull for workspace packages, and
445443
// ParseExported for others.
446444
TypecheckWorkspace
447-
// TypecheckAll means ParseFull for workspace packages, and both Full and
448-
// Exported for others. Only valid for some functions.
449-
TypecheckAll
450445
)
451446

452447
type VersionedFileHandle interface {

gopls/internal/regtest/misc/references_test.go

+119
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ package misc
66

77
import (
88
"fmt"
9+
"sort"
910
"strings"
1011
"testing"
1112

13+
"github.com/google/go-cmp/cmp"
14+
"golang.org/x/tools/gopls/internal/lsp/protocol"
1215
. "golang.org/x/tools/gopls/internal/lsp/regtest"
1316
)
1417

@@ -169,3 +172,119 @@ func main() {
169172
}
170173
})
171174
}
175+
176+
// Test for golang/go#43144.
177+
//
178+
// Verify that we search for references and implementations in intermediate
179+
// test variants.
180+
func TestReferencesInTestVariants(t *testing.T) {
181+
const files = `
182+
-- go.mod --
183+
module foo.mod
184+
185+
go 1.12
186+
-- foo/foo.go --
187+
package foo
188+
189+
import "foo.mod/bar"
190+
191+
const Foo = 42
192+
193+
type T int
194+
type Interface interface{ M() }
195+
196+
func _() {
197+
_ = bar.Blah
198+
}
199+
200+
-- bar/bar.go --
201+
package bar
202+
203+
var Blah = 123
204+
205+
-- bar/bar_test.go --
206+
package bar
207+
208+
type Mer struct{}
209+
func (Mer) M() {}
210+
211+
func TestBar() {
212+
_ = Blah
213+
}
214+
-- bar/bar_x_test.go --
215+
package bar_test
216+
217+
import (
218+
"foo.mod/bar"
219+
"foo.mod/foo"
220+
)
221+
222+
type Mer struct{}
223+
func (Mer) M() {}
224+
225+
func _() {
226+
_ = bar.Blah
227+
_ = foo.Foo
228+
}
229+
`
230+
231+
Run(t, files, func(t *testing.T, env *Env) {
232+
env.OpenFile("foo/foo.go")
233+
234+
// Helper to map locations relative file paths.
235+
fileLocations := func(locs []protocol.Location) []string {
236+
var got []string
237+
for _, loc := range locs {
238+
got = append(got, env.Sandbox.Workdir.URIToPath(loc.URI))
239+
}
240+
sort.Strings(got)
241+
return got
242+
}
243+
244+
refTests := []struct {
245+
re string
246+
wantRefs []string
247+
}{
248+
// Blah is referenced:
249+
// - inside the foo.mod/bar (ordinary) package
250+
// - inside the foo.mod/bar [foo.mod/bar.test] test variant package
251+
// - from the foo.mod/bar_test [foo.mod/bar.test] x_test package
252+
// - from the foo.mod/foo package
253+
{"Blah", []string{"bar/bar.go", "bar/bar_test.go", "bar/bar_x_test.go", "foo/foo.go"}},
254+
255+
// Foo is referenced in bar_x_test.go via the intermediate test variant
256+
// foo.mod/foo [foo.mod/bar.test].
257+
{"Foo", []string{"bar/bar_x_test.go", "foo/foo.go"}},
258+
}
259+
260+
for _, test := range refTests {
261+
pos := env.RegexpSearch("foo/foo.go", test.re)
262+
refs := env.References("foo/foo.go", pos)
263+
264+
got := fileLocations(refs)
265+
if diff := cmp.Diff(test.wantRefs, got); diff != "" {
266+
t.Errorf("References(%q) returned unexpected diff (-want +got):\n%s", test.re, diff)
267+
}
268+
}
269+
270+
implTests := []struct {
271+
re string
272+
wantImpls []string
273+
}{
274+
// Interface is implemented both in foo.mod/bar [foo.mod/bar.test] (which
275+
// doesn't import foo), and in foo.mod/bar_test [foo.mod/bar.test], which
276+
// imports the test variant of foo.
277+
{"Interface", []string{"bar/bar_test.go", "bar/bar_x_test.go"}},
278+
}
279+
280+
for _, test := range implTests {
281+
pos := env.RegexpSearch("foo/foo.go", test.re)
282+
refs := env.Implementations("foo/foo.go", pos)
283+
284+
got := fileLocations(refs)
285+
if diff := cmp.Diff(test.wantImpls, got); diff != "" {
286+
t.Errorf("Implementations(%q) returned unexpected diff (-want +got):\n%s", test.re, diff)
287+
}
288+
}
289+
})
290+
}

0 commit comments

Comments
 (0)