Skip to content

Commit ff6edae

Browse files
Add LSP references / {workspace,document} symbol tests (#4263)
1 parent 53c0c58 commit ff6edae

File tree

11 files changed

+594
-0
lines changed

11 files changed

+594
-0
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2020-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package buflsp_test
16+
17+
import (
18+
"path/filepath"
19+
"slices"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
"go.lsp.dev/protocol"
25+
)
26+
27+
func TestDocumentSymbol(t *testing.T) {
28+
t.Parallel()
29+
30+
ctx := t.Context()
31+
32+
testProtoPath, err := filepath.Abs("testdata/symbols/symbols.proto")
33+
require.NoError(t, err)
34+
35+
clientJSONConn, testURI := setupLSPServer(t, testProtoPath)
36+
37+
type symbolInfo struct {
38+
name string
39+
kind protocol.SymbolKind
40+
line uint32
41+
deprecated bool
42+
}
43+
tests := []struct {
44+
name string
45+
expectedSymbols []symbolInfo
46+
}{
47+
{
48+
name: "all_document_symbols",
49+
expectedSymbols: []symbolInfo{
50+
{name: "symbols.v1.Document", kind: protocol.SymbolKindClass, line: 4}, // message Document
51+
{name: "symbols.v1.Document.id", kind: protocol.SymbolKindField, line: 5}, // string id
52+
{name: "symbols.v1.Document.title", kind: protocol.SymbolKindField, line: 6}, // string title
53+
{name: "symbols.v1.Document.status", kind: protocol.SymbolKindField, line: 7}, // Status status
54+
{name: "symbols.v1.Document.metadata", kind: protocol.SymbolKindField, line: 8}, // Metadata metadata
55+
{name: "symbols.v1.Document.Metadata", kind: protocol.SymbolKindClass, line: 9}, // message Metadata (nested)
56+
{name: "symbols.v1.Document.Metadata.author", kind: protocol.SymbolKindField, line: 10}, // string author
57+
{name: "symbols.v1.Document.Metadata.created_at", kind: protocol.SymbolKindField, line: 11}, // int64 created_at
58+
{name: "symbols.v1.Status", kind: protocol.SymbolKindEnum, line: 15}, // enum Status
59+
{name: "symbols.v1.STATUS_UNSPECIFIED", kind: protocol.SymbolKindEnumMember, line: 16}, // STATUS_UNSPECIFIED = 0
60+
{name: "symbols.v1.STATUS_DRAFT", kind: protocol.SymbolKindEnumMember, line: 17}, // STATUS_DRAFT = 1
61+
{name: "symbols.v1.STATUS_PUBLISHED", kind: protocol.SymbolKindEnumMember, line: 18}, // STATUS_PUBLISHED = 2
62+
{name: "symbols.v1.DocumentService", kind: protocol.SymbolKindInterface, line: 21}, // service DocumentService
63+
{name: "symbols.v1.DocumentService.GetDocument", kind: protocol.SymbolKindMethod, line: 22}, // rpc GetDocument
64+
{name: "symbols.v1.DocumentService.CreateDocument", kind: protocol.SymbolKindMethod, line: 23}, // rpc CreateDocument
65+
{name: "symbols.v1.GetDocumentRequest", kind: protocol.SymbolKindClass, line: 26}, // message GetDocumentRequest
66+
{name: "symbols.v1.GetDocumentRequest.document_id", kind: protocol.SymbolKindField, line: 27}, // string document_id
67+
{name: "symbols.v1.GetDocumentResponse", kind: protocol.SymbolKindClass, line: 30}, // message GetDocumentResponse
68+
{name: "symbols.v1.GetDocumentResponse.document", kind: protocol.SymbolKindField, line: 31}, // Document document
69+
{name: "symbols.v1.CreateDocumentRequest", kind: protocol.SymbolKindClass, line: 34}, // message CreateDocumentRequest
70+
{name: "symbols.v1.CreateDocumentRequest.document", kind: protocol.SymbolKindField, line: 35}, // Document document
71+
{name: "symbols.v1.CreateDocumentResponse", kind: protocol.SymbolKindClass, line: 38}, // message CreateDocumentResponse
72+
{name: "symbols.v1.CreateDocumentResponse.document", kind: protocol.SymbolKindField, line: 39}, // Document document
73+
{name: "symbols.v1.LegacyDocument", kind: protocol.SymbolKindClass, line: 42, deprecated: true}, // message LegacyDocument (deprecated)
74+
{name: "symbols.v1.LegacyDocument.id", kind: protocol.SymbolKindField, line: 44}, // string id
75+
},
76+
},
77+
}
78+
79+
for _, tt := range tests {
80+
t.Run(tt.name, func(t *testing.T) {
81+
t.Parallel()
82+
83+
var symbols []protocol.SymbolInformation
84+
_, symErr := clientJSONConn.Call(ctx, protocol.MethodTextDocumentDocumentSymbol, protocol.DocumentSymbolParams{
85+
TextDocument: protocol.TextDocumentIdentifier{
86+
URI: testURI,
87+
},
88+
}, &symbols)
89+
require.NoError(t, symErr)
90+
91+
require.Len(t, symbols, len(tt.expectedSymbols))
92+
93+
for _, expectedSymbol := range tt.expectedSymbols {
94+
idx := slices.IndexFunc(symbols, func(s protocol.SymbolInformation) bool {
95+
return s.Name == expectedSymbol.name
96+
})
97+
require.NotEqual(t, -1, idx, "expected to find symbol %s", expectedSymbol.name)
98+
found := symbols[idx]
99+
assert.Equal(t, expectedSymbol.kind, found.Kind, "symbol %s has wrong kind", expectedSymbol.name)
100+
assert.Equal(t, testURI, found.Location.URI, "symbol %s has wrong URI", expectedSymbol.name)
101+
assert.Equal(t, expectedSymbol.line, found.Location.Range.Start.Line, "symbol %s has wrong line number", expectedSymbol.name)
102+
assert.Equal(t, expectedSymbol.deprecated, found.Deprecated, "symbol %s has wrong deprecated status", expectedSymbol.name)
103+
}
104+
})
105+
}
106+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright 2020-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package buflsp_test
16+
17+
import (
18+
"path/filepath"
19+
"slices"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
"go.lsp.dev/protocol"
25+
"go.lsp.dev/uri"
26+
)
27+
28+
func TestReferences(t *testing.T) {
29+
t.Parallel()
30+
31+
ctx := t.Context()
32+
33+
testProtoPath, err := filepath.Abs("testdata/references/references.proto")
34+
require.NoError(t, err)
35+
36+
typesProtoPath, err := filepath.Abs("testdata/references/types.proto")
37+
require.NoError(t, err)
38+
39+
clientJSONConn, testURI := setupLSPServer(t, testProtoPath)
40+
typesURI := uri.New(typesProtoPath)
41+
42+
type refLocation struct {
43+
uri protocol.URI
44+
line uint32
45+
}
46+
tests := []struct {
47+
name string
48+
targetURI protocol.URI
49+
line uint32
50+
character uint32
51+
includeDeclaration bool
52+
expectedReferences []refLocation
53+
}{
54+
{
55+
name: "references_to_item_message",
56+
targetURI: testURI,
57+
line: 6,
58+
character: 8,
59+
includeDeclaration: true,
60+
expectedReferences: []refLocation{
61+
{uri: testURI, line: 6}, // message Item
62+
{uri: testURI, line: 10}, // repeated Item related
63+
{uri: testURI, line: 15}, // repeated Item items in Container
64+
{uri: testURI, line: 29}, // Item item in GetItemResponse
65+
{uri: testURI, line: 37}, // repeated Item items in ListItemsResponse
66+
},
67+
},
68+
{
69+
name: "references_to_item_message_no_declaration",
70+
targetURI: testURI,
71+
line: 6,
72+
character: 8,
73+
includeDeclaration: false,
74+
expectedReferences: []refLocation{
75+
{uri: testURI, line: 10}, // repeated Item related
76+
{uri: testURI, line: 15}, // repeated Item items in Container
77+
{uri: testURI, line: 29}, // Item item in GetItemResponse
78+
{uri: testURI, line: 37}, // repeated Item items in ListItemsResponse
79+
},
80+
},
81+
{
82+
name: "references_to_color_enum_imported",
83+
targetURI: typesURI,
84+
line: 4,
85+
character: 5,
86+
includeDeclaration: true,
87+
expectedReferences: []refLocation{
88+
{uri: typesURI, line: 4}, // enum Color
89+
{uri: testURI, line: 8}, // Color color in Item
90+
{uri: testURI, line: 16}, // Color default_color in Container
91+
},
92+
},
93+
{
94+
name: "references_to_container_message",
95+
targetURI: testURI,
96+
line: 13,
97+
character: 8,
98+
includeDeclaration: true,
99+
expectedReferences: []refLocation{
100+
{uri: testURI, line: 13}, // message Container
101+
},
102+
},
103+
{
104+
name: "references_to_label_imported_type",
105+
targetURI: typesURI,
106+
line: 10,
107+
character: 8,
108+
includeDeclaration: true,
109+
expectedReferences: []refLocation{
110+
{uri: typesURI, line: 10}, // message Label
111+
{uri: testURI, line: 9}, // Label label in Item
112+
},
113+
},
114+
{
115+
name: "references_to_get_item_request",
116+
targetURI: testURI,
117+
line: 24,
118+
character: 8,
119+
includeDeclaration: true,
120+
expectedReferences: []refLocation{
121+
{uri: testURI, line: 24}, // message GetItemRequest
122+
{uri: testURI, line: 20}, // rpc GetItem(GetItemRequest)
123+
},
124+
},
125+
{
126+
name: "references_to_service",
127+
targetURI: testURI,
128+
line: 19,
129+
character: 8,
130+
includeDeclaration: true,
131+
expectedReferences: []refLocation{
132+
{uri: testURI, line: 19}, // service ItemService
133+
},
134+
},
135+
}
136+
137+
for _, tt := range tests {
138+
t.Run(tt.name, func(t *testing.T) {
139+
t.Parallel()
140+
141+
var locations []protocol.Location
142+
_, refErr := clientJSONConn.Call(ctx, protocol.MethodTextDocumentReferences, protocol.ReferenceParams{
143+
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
144+
TextDocument: protocol.TextDocumentIdentifier{
145+
URI: tt.targetURI,
146+
},
147+
Position: protocol.Position{
148+
Line: tt.line,
149+
Character: tt.character,
150+
},
151+
},
152+
Context: protocol.ReferenceContext{
153+
IncludeDeclaration: tt.includeDeclaration,
154+
},
155+
}, &locations)
156+
require.NoError(t, refErr)
157+
158+
require.Len(t, locations, len(tt.expectedReferences))
159+
160+
for _, expectedRef := range tt.expectedReferences {
161+
idx := slices.IndexFunc(locations, func(loc protocol.Location) bool {
162+
return loc.URI == expectedRef.uri && loc.Range.Start.Line == expectedRef.line
163+
})
164+
assert.NotEqual(t, -1, idx, "expected reference at %s:%d not found", expectedRef.uri, expectedRef.line)
165+
}
166+
})
167+
}
168+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
version: v2
2+
modules:
3+
- path: .
4+
lint:
5+
use:
6+
- STANDARD
7+
breaking:
8+
use:
9+
- FILE
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
syntax = "proto3";
2+
3+
package references.v1;
4+
5+
import "types.proto";
6+
7+
message Item {
8+
string id = 1;
9+
Color color = 2;
10+
Label label = 3;
11+
repeated Item related = 4;
12+
}
13+
14+
message Container {
15+
string name = 1;
16+
repeated Item items = 2;
17+
Color default_color = 3;
18+
}
19+
20+
service ItemService {
21+
rpc GetItem(GetItemRequest) returns (GetItemResponse);
22+
rpc ListItems(ListItemsRequest) returns (ListItemsResponse);
23+
}
24+
25+
message GetItemRequest {
26+
string item_id = 1;
27+
}
28+
29+
message GetItemResponse {
30+
Item item = 1;
31+
}
32+
33+
message ListItemsRequest {
34+
string container_name = 1;
35+
}
36+
37+
message ListItemsResponse {
38+
repeated Item items = 1;
39+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
syntax = "proto3";
2+
3+
package references.v1;
4+
5+
enum Color {
6+
COLOR_UNSPECIFIED = 0;
7+
COLOR_RED = 1;
8+
COLOR_BLUE = 2;
9+
}
10+
11+
message Label {
12+
string key = 1;
13+
string value = 2;
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
version: v2
2+
modules:
3+
- path: .
4+
lint:
5+
use:
6+
- STANDARD
7+
breaking:
8+
use:
9+
- FILE
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
syntax = "proto3";
2+
3+
package symbols.v1;
4+
5+
message Document {
6+
string id = 1;
7+
string title = 2;
8+
Status status = 3;
9+
Metadata metadata = 4;
10+
message Metadata {
11+
string author = 1;
12+
int64 created_at = 2;
13+
}
14+
}
15+
16+
enum Status {
17+
STATUS_UNSPECIFIED = 0;
18+
STATUS_DRAFT = 1;
19+
STATUS_PUBLISHED = 2;
20+
}
21+
22+
service DocumentService {
23+
rpc GetDocument(GetDocumentRequest) returns (GetDocumentResponse);
24+
rpc CreateDocument(CreateDocumentRequest) returns (CreateDocumentResponse);
25+
}
26+
27+
message GetDocumentRequest {
28+
string document_id = 1;
29+
}
30+
31+
message GetDocumentResponse {
32+
Document document = 1;
33+
}
34+
35+
message CreateDocumentRequest {
36+
Document document = 1;
37+
}
38+
39+
message CreateDocumentResponse {
40+
Document document = 1;
41+
}
42+
43+
message LegacyDocument {
44+
option deprecated = true;
45+
string id = 1;
46+
}

0 commit comments

Comments
 (0)