Skip to content

Commit 9fbd0cc

Browse files
committed
internal/lsp/lsprpc: add test for definition outside of workspace
Add regression tests for GoToDefinition. In particular, exercise the panic from golang/go#37045. Updates golang/go#37045 Updates golang/go#36879 Change-Id: I67b562acd293f47907de0435c14b62c1a22cf2ee Reviewed-on: https://go-review.googlesource.com/c/tools/+/218322 Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
1 parent babff93 commit 9fbd0cc

File tree

6 files changed

+169
-33
lines changed

6 files changed

+169
-33
lines changed

Diff for: internal/lsp/fake/client.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,7 @@ func (c *Client) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceE
9797
return &protocol.ApplyWorkspaceEditResponse{FailureReason: "Edit.Changes is unsupported"}, nil
9898
}
9999
for _, change := range params.Edit.DocumentChanges {
100-
path, err := c.ws.URIToPath(change.TextDocument.URI)
101-
if err != nil {
102-
return nil, err
103-
}
100+
path := c.ws.URIToPath(change.TextDocument.URI)
104101
var edits []Edit
105102
for _, lspEdit := range change.Edits {
106103
edits = append(edits, fromProtocolTextEdit(lspEdit))

Diff for: internal/lsp/fake/edit.go

+15-15
Original file line numberDiff line numberDiff line change
@@ -54,30 +54,30 @@ func fromProtocolTextEdit(textEdit protocol.TextEdit) Edit {
5454
}
5555
}
5656

57+
// inText reports whether p is a valid position in the text buffer.
58+
func inText(p Pos, content []string) bool {
59+
if p.Line < 0 || p.Line >= len(content) {
60+
return false
61+
}
62+
// Note the strict right bound: the column indexes character _separators_,
63+
// not characters.
64+
if p.Column < 0 || p.Column > len(content[p.Line]) {
65+
return false
66+
}
67+
return true
68+
}
69+
5770
// editContent implements a simplistic, inefficient algorithm for applying text
5871
// edits to our buffer representation. It returns an error if the edit is
5972
// invalid for the current content.
6073
func editContent(content []string, edit Edit) ([]string, error) {
6174
if edit.End.Line < edit.Start.Line || (edit.End.Line == edit.Start.Line && edit.End.Column < edit.Start.Column) {
6275
return nil, fmt.Errorf("invalid edit: end %v before start %v", edit.End, edit.Start)
6376
}
64-
// inText reports whether a position is within the bounds of the current
65-
// text.
66-
inText := func(p Pos) bool {
67-
if p.Line < 0 || p.Line >= len(content) {
68-
return false
69-
}
70-
// Note the strict right bound: the column indexes character _separators_,
71-
// not characters.
72-
if p.Column < 0 || p.Column > len(content[p.Line]) {
73-
return false
74-
}
75-
return true
76-
}
77-
if !inText(edit.Start) {
77+
if !inText(edit.Start, content) {
7878
return nil, fmt.Errorf("start position %v is out of bounds", edit.Start)
7979
}
80-
if !inText(edit.End) {
80+
if !inText(edit.End, content) {
8181
return nil, fmt.Errorf("end position %v is out of bounds", edit.End)
8282
}
8383
// Splice the edit text in between the first and last lines of the edit.

Diff for: internal/lsp/fake/editor.go

+38-3
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,41 @@ func (e *Editor) doEdits(ctx context.Context, path string, edits []Edit) (*proto
322322
return params, nil
323323
}
324324

325-
// TODO: expose more client functionality, for example GoToDefinition, Hover,
326-
// CodeAction, Rename, Completion, etc. setting the content of an entire
327-
// buffer, etc.
325+
// GoToDefinition jumps to the definition of the symbol at the given position
326+
// in an open buffer.
327+
func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) {
328+
if err := e.checkBufferPosition(path, pos); err != nil {
329+
return "", Pos{}, err
330+
}
331+
params := &protocol.DefinitionParams{}
332+
params.TextDocument.URI = e.ws.URI(path)
333+
params.Position = pos.toProtocolPosition()
334+
335+
resp, err := e.server.Definition(ctx, params)
336+
if err != nil {
337+
return "", Pos{}, fmt.Errorf("Definition: %v", err)
338+
}
339+
if len(resp) == 0 {
340+
return "", Pos{}, nil
341+
}
342+
newPath := e.ws.URIToPath(resp[0].URI)
343+
newPos := fromProtocolPosition(resp[0].Range.Start)
344+
e.OpenFile(ctx, newPath)
345+
return newPath, newPos, nil
346+
}
347+
348+
func (e *Editor) checkBufferPosition(path string, pos Pos) error {
349+
e.mu.Lock()
350+
defer e.mu.Unlock()
351+
buf, ok := e.buffers[path]
352+
if !ok {
353+
return fmt.Errorf("buffer %q is not open", path)
354+
}
355+
if !inText(pos, buf.content) {
356+
return fmt.Errorf("position %v is invalid in buffer %q", pos, path)
357+
}
358+
return nil
359+
}
360+
361+
// TODO: expose more client functionality, for example Hover, CodeAction,
362+
// Rename, Completion, etc. setting the content of an entire buffer, etc.

Diff for: internal/lsp/fake/workspace.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ func (w *Workspace) AddWatcher(watcher func(context.Context, []FileEvent)) {
8686
// filePath returns the absolute filesystem path to a the workspace-relative
8787
// path.
8888
func (w *Workspace) filePath(path string) string {
89+
fp := filepath.FromSlash(path)
90+
if filepath.IsAbs(fp) {
91+
return fp
92+
}
8993
return filepath.Join(w.workdir, filepath.FromSlash(path))
9094
}
9195

@@ -94,13 +98,15 @@ func (w *Workspace) URI(path string) protocol.DocumentURI {
9498
return toURI(w.filePath(path))
9599
}
96100

97-
// URIToPath converts a uri to a workspace-relative path.
98-
func (w *Workspace) URIToPath(uri protocol.DocumentURI) (string, error) {
99-
prefix := w.RootURI() + "/"
100-
if !strings.HasPrefix(uri, prefix) {
101-
return "", fmt.Errorf("uri %q outside of workspace", uri)
101+
// URIToPath converts a uri to a workspace-relative path (or an absolute path,
102+
// if the uri is outside of the workspace).
103+
func (w *Workspace) URIToPath(uri protocol.DocumentURI) string {
104+
root := w.RootURI() + "/"
105+
if strings.HasPrefix(uri, root) {
106+
return strings.TrimPrefix(uri, root)
102107
}
103-
return strings.TrimPrefix(uri, prefix), nil
108+
filename := span.NewURI(string(uri)).Filename()
109+
return filepath.ToSlash(filename)
104110
}
105111

106112
func toURI(fp string) protocol.DocumentURI {

Diff for: internal/lsp/lsprpc/definition_test.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2020 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 lsprpc
6+
7+
import (
8+
"fmt"
9+
"path"
10+
"testing"
11+
"time"
12+
13+
"golang.org/x/tools/internal/lsp/fake"
14+
)
15+
16+
const internalDefinition = `
17+
-- go.mod --
18+
module mod
19+
20+
go 1.12
21+
-- main.go --
22+
package main
23+
24+
import "fmt"
25+
26+
func main() {
27+
fmt.Println(message)
28+
}
29+
-- const.go --
30+
package main
31+
32+
const message = "Hello World."
33+
`
34+
35+
func TestGoToInternalDefinition(t *testing.T) {
36+
t.Parallel()
37+
ctx, env, cleanup := setupEnv(t, internalDefinition)
38+
defer cleanup()
39+
40+
if err := env.editor.OpenFile(ctx, "main.go"); err != nil {
41+
t.Fatal(err)
42+
}
43+
name, pos, err := env.editor.GoToDefinition(ctx, "main.go", fake.Pos{Line: 5, Column: 13})
44+
if err != nil {
45+
t.Fatal(err)
46+
}
47+
if want := "const.go"; name != want {
48+
t.Errorf("GoToDefinition: got file %q, want %q", name, want)
49+
}
50+
if want := (fake.Pos{Line: 2, Column: 6}); pos != want {
51+
t.Errorf("GoToDefinition: got position %v, want %v", pos, want)
52+
}
53+
}
54+
55+
const stdlibDefinition = `
56+
-- go.mod --
57+
module mod
58+
59+
go 1.12
60+
-- main.go --
61+
package main
62+
63+
import (
64+
"fmt"
65+
"time"
66+
)
67+
68+
func main() {
69+
fmt.Println(time.Now())
70+
}`
71+
72+
func TestGoToStdlibDefinition(t *testing.T) {
73+
t.Parallel()
74+
ctx, env, cleanup := setupEnv(t, stdlibDefinition)
75+
defer cleanup()
76+
77+
if err := env.editor.OpenFile(ctx, "main.go"); err != nil {
78+
t.Fatal(err)
79+
}
80+
name, pos, err := env.editor.GoToDefinition(ctx, "main.go", fake.Pos{Line: 8, Column: 19})
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
fmt.Println(time.Now())
85+
if got, want := path.Base(name), "time.go"; got != want {
86+
t.Errorf("GoToDefinition: got file %q, want %q", name, want)
87+
}
88+
89+
// Test that we can jump to definition from outside our workspace.
90+
// See golang.org/issues/37045.
91+
newName, newPos, err := env.editor.GoToDefinition(ctx, name, pos)
92+
if err != nil {
93+
t.Fatal(err)
94+
}
95+
if newName != name {
96+
t.Errorf("GoToDefinition is not idempotent: got %q, want %q", newName, name)
97+
}
98+
if newPos != pos {
99+
t.Errorf("GoToDefinition is not idempotent: got %v, want %v", newPos, pos)
100+
}
101+
}

Diff for: internal/lsp/lsprpc/diagnostics_test.go

+2-5
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func setupEnv(t *testing.T, txt string) (context.Context, testEnvironment, func(
4343
t.Helper()
4444
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
4545

46-
ws, err := fake.NewWorkspace("get-diagnostics", []byte(txt))
46+
ws, err := fake.NewWorkspace("lsprpc", []byte(txt))
4747
if err != nil {
4848
t.Fatal(err)
4949
}
@@ -100,10 +100,7 @@ func (w diagnosticsWatcher) await(ctx context.Context, expected ...string) (map[
100100
case <-ctx.Done():
101101
return nil, ctx.Err()
102102
case d := <-w.diagnostics:
103-
pth, err := w.ws.URIToPath(d.URI)
104-
if err != nil {
105-
return nil, err
106-
}
103+
pth := w.ws.URIToPath(d.URI)
107104
if expectedSet[pth] {
108105
got[pth] = d
109106
}

0 commit comments

Comments
 (0)