diff --git a/CHANGELOG.md b/CHANGELOG.md index d12f5bb901..97a3440d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Fix `buf format` dropping comments next to commas or semicolons in message literals. - Fix compilation failing to resolve symbols re-exported through `import public` when the re-exporting file also reaches those symbols through a non-public import. +- Fix LSP finding only a subset of references to symbols declared in + dependencies, including well-known types. ## [v1.72.0] - 2026-07-17 diff --git a/private/buf/buflsp/buflsp.go b/private/buf/buflsp/buflsp.go index 2f05480017..28c8df83de 100644 --- a/private/buf/buflsp/buflsp.go +++ b/private/buf/buflsp/buflsp.go @@ -100,6 +100,7 @@ func Serve( } lsp.fileManager = newFileManager(lsp) lsp.workspaceManager = newWorkspaceManager(lsp) + lsp.referenceIndex = newReferenceIndex() lsp.bufYAMLManager = newBufYAMLManager(lsp) lsp.bufGenYAMLManager = newBufGenYAMLManager(lsp) lsp.bufPolicyYAMLManager = newBufPolicyYAMLManager() @@ -137,6 +138,7 @@ type lsp struct { wasmRuntime wasm.Runtime fileManager *fileManager workspaceManager *workspaceManager + referenceIndex *referenceIndex bufYAMLManager *bufYAMLManager bufGenYAMLManager *bufGenYAMLManager bufPolicyYAMLManager *bufPolicyYAMLManager diff --git a/private/buf/buflsp/file.go b/private/buf/buflsp/file.go index 3ee402f47f..77a626e11e 100644 --- a/private/buf/buflsp/file.go +++ b/private/buf/buflsp/file.go @@ -63,7 +63,6 @@ type file struct { ir *ir.File referenceableSymbols map[ir.FullName]*symbol - referenceSymbols []*symbol symbols []*symbol irReport *report.Report // IR diagnostic report for code actions diagnostics []protocol.Diagnostic // Converted LSP diagnostics @@ -98,6 +97,8 @@ func (f *file) Reset(ctx context.Context) { f.workspace.Release() f.workspace = nil } + // Drop this file's references from the index before the file is zeroed. + f.lsp.referenceIndex.RemoveFile(f.uri) // Evict the query key if there is a query cached on the file. We cache the [queries.File] // query since this allows the executor to evict all dependent queries, e.g. AST and IR. f.lsp.queryExecutor.Evict(f.queryFileKeys()...) @@ -354,7 +355,6 @@ func (f *file) IndexSymbols(ctx context.Context) { // Throw away all the old symbols and rebuild symbols unconditionally. This is because if // this file depends on a file that has since been modified, we may need to update references. f.symbols = nil - f.referenceSymbols = nil f.referenceableSymbols = make(map[ir.FullName]*symbol) // Process all imports as symbols @@ -365,7 +365,6 @@ func (f *file) IndexSymbols(ctx context.Context) { resolved, unresolved := f.indexSymbols() f.symbols = append(f.symbols, resolved...) f.symbols = append(f.symbols, unresolved...) - f.referenceSymbols = append(f.referenceSymbols, unresolved...) // Index all referenceable symbols for _, sym := range resolved { @@ -376,47 +375,29 @@ func (f *file) IndexSymbols(ctx context.Context) { f.referenceableSymbols[sym.ir.FullName()] = sym } - // TODO: this could use a refactor, probably. - // - // Resolve all unresolved symbols from this file + // Resolve all unresolved symbols from this file, and record the references they make in + // the reference index. References are keyed by definition site, so recording does not + // depend on the order files are indexed in. + var references map[referenceKey][]*symbol + addReference := func(def ast.DeclDef, fullName ir.FullName, sym *symbol) { + key, ok := newReferenceKey(def, fullName) + if !ok { + return + } + if references == nil { + references = make(map[referenceKey][]*symbol) + } + references[key] = append(references[key], sym) + } for _, sym := range unresolved { switch kind := sym.kind.(type) { case *reference: - def := f.resolveASTDefinition(kind.def, kind.fullName) - sym.def = def - if def == nil { - // In the case where the symbol is not resolved, we continue - continue - } - referenceable, ok := def.kind.(*referenceable) - if !ok { - // This shouldn't happen, logging a warning - f.lsp.logger.Warn( - "found non-referenceable symbol in index", - slog.String("file", f.uri.Filename()), - slog.Any("symbol", def), - ) - continue - } - referenceable.references = append(referenceable.references, sym) + sym.def = f.resolveASTDefinition(kind.def, kind.fullName) + addReference(kind.def, kind.fullName, sym) case *option: - def := f.resolveASTDefinition(kind.def, kind.defFullName) - sym.def = def - if def != nil { - referenceable, ok := def.kind.(*referenceable) - if !ok { - // This shouldn't happen, logging a warning - f.lsp.logger.Warn( - "found non-referenceable symbol in index", - slog.String("file", f.uri.Filename()), - slog.Any("symbol", def), - ) - } else { - referenceable.references = append(referenceable.references, sym) - } - } - typeDef := f.resolveASTDefinition(kind.typeDef, kind.typeDefFullName) - sym.typeDef = typeDef + sym.def = f.resolveASTDefinition(kind.def, kind.defFullName) + addReference(kind.def, kind.defFullName, sym) + sym.typeDef = f.resolveASTDefinition(kind.typeDef, kind.typeDefFullName) default: // This shouldn't happen, logging a warning f.lsp.logger.Warn( @@ -426,58 +407,7 @@ func (f *file) IndexSymbols(ctx context.Context) { ) } } - - // Resolve all references outside of this file to symbols in this file - for _, file := range f.workspace.PathToFile() { - if f == file { - continue // ignore self - } - for _, sym := range file.referenceSymbols { - var fullName ir.FullName - switch kind := sym.kind.(type) { - case *reference: - if kind.def.Span().Path() != f.objectInfo.LocalPath() { - continue - } - fullName = kind.fullName - case *option: - if kind.def.Span().Path() != f.objectInfo.LocalPath() { - continue - } - fullName = kind.defFullName - default: - // This shouldn't happen, logging a warning - f.lsp.logger.Warn( - "found unresolved non-reference and non-option symbol", - slog.String("file", f.uri.Filename()), - slog.Any("symbol", sym), - ) - continue - } - def, ok := f.referenceableSymbols[fullName] - if !ok { - // This shouldn't happen, if a symbol is pointing at this file, all definitions - // should be resolved, logging a warning - f.lsp.logger.Warn( - "found reference to unknown symbol", - slog.String("file", f.uri.Filename()), - slog.Any("reference", sym), - ) - continue - } - referenceable, ok := def.kind.(*referenceable) - if !ok { - // This shouldn't happen, logging a warning - f.lsp.logger.Warn( - "found non-referenceable symbol in index", - slog.String("file", f.uri.Filename()), - slog.Any("symbol", def), - ) - continue - } - referenceable.references = append(referenceable.references, sym) - } - } + f.lsp.referenceIndex.SetFile(f.uri, references) // Finally, sort the symbols in position order, with shorter symbols sorting smaller. slices.SortFunc(f.symbols, func(s1, s2 *symbol) int { diff --git a/private/buf/buflsp/reference_index.go b/private/buf/buflsp/reference_index.go new file mode 100644 index 0000000000..1037f51b0f --- /dev/null +++ b/private/buf/buflsp/reference_index.go @@ -0,0 +1,141 @@ +// Copyright 2020-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file defines the reverse index from symbol definitions to their references. + +package buflsp + +import ( + "cmp" + "slices" + + "github.com/bufbuild/protocompile/experimental/ast" + "github.com/bufbuild/protocompile/experimental/ir" + "go.lsp.dev/protocol" +) + +// referenceKey identifies a symbol definition site: the path of the declaring file as it +// appears in source spans, plus the symbol's fully-qualified name. +type referenceKey struct { + path string + fullName ir.FullName +} + +// newReferenceKey builds the key for the definition an unresolved symbol names. Returns +// false if the definition has no span to take a path from, such as a zero [ast.DeclDef]. +func newReferenceKey(def ast.DeclDef, fullName ir.FullName) (referenceKey, bool) { + path := def.Span().Path() + if path == "" || fullName == "" { + return referenceKey{}, false + } + return referenceKey{path: path, fullName: fullName}, true +} + +// newReferenceKeyForDeclaration builds the key for a declaration symbol. This is the only +// other way to construct a key, and derives the path from the same source as spans do, so +// lookups cannot diverge from what [newReferenceKey] indexed. +func newReferenceKeyForDeclaration(declaration *symbol) (referenceKey, bool) { + if declaration.file == nil || declaration.file.file == nil || declaration.ir.IsZero() { + return referenceKey{}, false + } + return referenceKey{ + path: declaration.file.file.Path(), + fullName: declaration.ir.FullName(), + }, true +} + +// referenceIndex is a reverse index from a symbol definition to the symbols referencing it, +// across all workspaces. References are grouped by the file containing them, so re-indexing +// a file replaces only that file's contribution. +// +// The index is not safe for concurrent use; it is protected by the [lsp] lock. +type referenceIndex struct { + keyToFileRefs map[referenceKey]map[protocol.URI][]*symbol + // uriToKeys records which definitions each file contributes references to, so that a + // file's contribution can be removed without scanning the whole index. + uriToKeys map[protocol.URI][]referenceKey +} + +// newReferenceIndex creates a new reference index. +func newReferenceIndex() *referenceIndex { + return &referenceIndex{ + keyToFileRefs: make(map[referenceKey]map[protocol.URI][]*symbol), + uriToKeys: make(map[protocol.URI][]referenceKey), + } +} + +// SetFile replaces all references contributed by the file at the given URI. +func (i *referenceIndex) SetFile(uri protocol.URI, references map[referenceKey][]*symbol) { + i.RemoveFile(uri) + if len(references) == 0 { + return + } + keys := make([]referenceKey, 0, len(references)) + for key, symbols := range references { + fileRefs, ok := i.keyToFileRefs[key] + if !ok { + fileRefs = make(map[protocol.URI][]*symbol, 1) + i.keyToFileRefs[key] = fileRefs + } + fileRefs[uri] = symbols + keys = append(keys, key) + } + i.uriToKeys[uri] = keys +} + +// RemoveFile drops all references contributed by the file at the given URI. +// +// This must be called when a file is evicted. The index holds symbols pointing back at +// their file, and an evicted file is zeroed, so entries left behind would resolve to +// locations with an empty URI. +func (i *referenceIndex) RemoveFile(uri protocol.URI) { + for _, key := range i.uriToKeys[uri] { + fileRefs, ok := i.keyToFileRefs[key] + if !ok { + continue + } + delete(fileRefs, uri) + if len(fileRefs) == 0 { + delete(i.keyToFileRefs, key) + } + } + delete(i.uriToKeys, uri) +} + +// References returns all symbols referencing the given definition, ordered by file URI and +// then by position so that results are stable across calls. +func (i *referenceIndex) References(key referenceKey) []*symbol { + fileRefs := i.keyToFileRefs[key] + if len(fileRefs) == 0 { + return nil + } + var symbols []*symbol + for _, fileSymbols := range fileRefs { + symbols = append(symbols, fileSymbols...) + } + slices.SortFunc(symbols, func(symbol1, symbol2 *symbol) int { + return cmp.Or( + cmp.Compare(symbol1.file.uri, symbol2.file.uri), + cmp.Compare(symbol1.span.Start, symbol2.span.Start), + cmp.Compare(symbol1.span.End, symbol2.span.End), + ) + }) + return symbols +} + +// FileReferences returns the symbols in the file at the given URI that reference the given +// definition. This is a direct lookup, cheap enough for per-cursor-move requests. +func (i *referenceIndex) FileReferences(key referenceKey, uri protocol.URI) []*symbol { + return i.keyToFileRefs[key][uri] +} diff --git a/private/buf/buflsp/references_test.go b/private/buf/buflsp/references_test.go index 55a0a58a3a..b3bc65aaed 100644 --- a/private/buf/buflsp/references_test.go +++ b/private/buf/buflsp/references_test.go @@ -15,6 +15,8 @@ package buflsp_test import ( + "context" + "os" "path/filepath" "slices" "testing" @@ -22,6 +24,7 @@ import ( "github.com/bufbuild/buf/private/buf/buflsp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.lsp.dev/jsonrpc2" "go.lsp.dev/protocol" ) @@ -140,22 +143,7 @@ func TestReferences(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - var locations []protocol.Location - _, refErr := clientJSONConn.Call(ctx, protocol.MethodTextDocumentReferences, protocol.ReferenceParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: tt.targetURI, - }, - Position: protocol.Position{ - Line: tt.line, - Character: tt.character, - }, - }, - Context: protocol.ReferenceContext{ - IncludeDeclaration: tt.includeDeclaration, - }, - }, &locations) - require.NoError(t, refErr) + locations := testRequestReferences(ctx, t, clientJSONConn, tt.targetURI, tt.line, tt.character, tt.includeDeclaration) require.Len(t, locations, len(tt.expectedReferences)) @@ -168,3 +156,145 @@ func TestReferences(t *testing.T) { }) } } + +func TestReferencesToDependency(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + firstProtoPath, err := filepath.Abs("testdata/references_dependency/first.proto") + require.NoError(t, err) + + secondProtoPath, err := filepath.Abs("testdata/references_dependency/second.proto") + require.NoError(t, err) + + sharedProtoPath, err := filepath.Abs("testdata/references_dependency/shared.proto") + require.NoError(t, err) + + // Only first.proto is opened. second.proto and shared.proto are indexed as part of the + // workspace and must still contribute and receive references. + clientJSONConn, firstURI := setupLSPServer(t, firstProtoPath) + secondURI := buflsp.FilePathToURI(secondProtoPath) + sharedURI := buflsp.FilePathToURI(sharedProtoPath) + + type refLocation struct { + uri protocol.URI + line uint32 + } + tests := []struct { + name string + line uint32 + character uint32 + includeDeclaration bool + expectedReferences []refLocation + }{ + { + name: "local_dependency_across_files", + line: 8, // Shared shared = 1; + character: 3, + includeDeclaration: false, + expectedReferences: []refLocation{ + {uri: firstURI, line: 8}, // Shared shared + {uri: firstURI, line: 10}, // repeated Shared others + {uri: secondURI, line: 8}, // Shared shared, in the unopened file + }, + }, + { + name: "local_dependency_with_declaration", + line: 8, + character: 3, + includeDeclaration: true, + expectedReferences: []refLocation{ + {uri: firstURI, line: 8}, + {uri: firstURI, line: 10}, + {uri: secondURI, line: 8}, + {uri: sharedURI, line: 4}, // message Shared + }, + }, + { + name: "wellknown_type_across_files", + line: 9, // google.protobuf.Timestamp created_at = 2; + character: 20, + includeDeclaration: false, + expectedReferences: []refLocation{ + {uri: firstURI, line: 9}, + {uri: secondURI, line: 9}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + locations := testRequestReferences(ctx, t, clientJSONConn, firstURI, tt.line, tt.character, tt.includeDeclaration) + + require.Len(t, locations, len(tt.expectedReferences)) + for _, expectedRef := range tt.expectedReferences { + idx := slices.IndexFunc(locations, func(loc protocol.Location) bool { + return loc.URI == expectedRef.uri && loc.Range.Start.Line == expectedRef.line + }) + assert.NotEqual(t, -1, idx, "expected reference at %s:%d not found", expectedRef.uri, expectedRef.line) + } + }) + } +} + +func TestReferencesStableAcrossReindex(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + firstProtoPath, err := filepath.Abs("testdata/references_dependency/first.proto") + require.NoError(t, err) + + clientJSONConn, firstURI := setupLSPServer(t, firstProtoPath) + + firstProtoContent, err := os.ReadFile(firstProtoPath) + require.NoError(t, err) + + // Position of the google.protobuf.Timestamp reference in first.proto. + const timestampLine, timestampCharacter = 9, 20 + + before := testRequestReferences(ctx, t, clientJSONConn, firstURI, timestampLine, timestampCharacter, true) + require.NotEmpty(t, before) + + // Re-send the file unchanged. This forces a full re-index without moving any position, so + // the reference set must come back identical. + require.NoError(t, clientJSONConn.Notify(ctx, protocol.MethodTextDocumentDidChange, &protocol.DidChangeTextDocumentParams{ + TextDocument: protocol.VersionedTextDocumentIdentifier{ + TextDocumentIdentifier: protocol.TextDocumentIdentifier{URI: firstURI}, + Version: 2, + }, + ContentChanges: []protocol.TextDocumentContentChangeEvent{{Text: string(firstProtoContent)}}, + })) + + after := testRequestReferences(ctx, t, clientJSONConn, firstURI, timestampLine, timestampCharacter, true) + + assert.ElementsMatch(t, before, after, "reference set changed after re-indexing") + assert.Len(t, slices.Compact(slices.Clone(after)), len(after), "re-indexing introduced duplicate references") +} + +// testRequestReferences sends a textDocument/references request and returns the locations. +func testRequestReferences( + ctx context.Context, + t *testing.T, + clientJSONConn jsonrpc2.Conn, + uri protocol.URI, + line uint32, + character uint32, + includeDeclaration bool, +) []protocol.Location { + t.Helper() + + var locations []protocol.Location + _, err := clientJSONConn.Call(ctx, protocol.MethodTextDocumentReferences, protocol.ReferenceParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: uri}, + Position: protocol.Position{Line: line, Character: character}, + }, + Context: protocol.ReferenceContext{IncludeDeclaration: includeDeclaration}, + }, &locations) + require.NoError(t, err) + return locations +} diff --git a/private/buf/buflsp/server.go b/private/buf/buflsp/server.go index b17599c22f..17a744fce4 100644 --- a/private/buf/buflsp/server.go +++ b/private/buf/buflsp/server.go @@ -517,14 +517,7 @@ func (s *server) References( if symbol == nil { return nil, nil } - // We deduplicate the references here in the case where a file's symbols have not yet - // been refreshed, but a new file with references to symbols in said file is opened. This - // can cause duplicate references to be appended and not all clients deduplicate the - // returned references. - // - // We also do not want to refresh all symbols in the workspace when a single file is - // interacted with, since that could be detrimental to performance. - return xslices.Deduplicate(symbol.References(params.Context.IncludeDeclaration)), nil + return symbol.References(params.Context.IncludeDeclaration), nil } // Completion is the entry point for code completion. diff --git a/private/buf/buflsp/symbol.go b/private/buf/buflsp/symbol.go index c4e02eeb0d..ed7aac2fdc 100644 --- a/private/buf/buflsp/symbol.go +++ b/private/buf/buflsp/symbol.go @@ -63,8 +63,7 @@ type kind interface { } type referenceable struct { - ast ast.DeclDef - references []*symbol + ast ast.DeclDef } type reference struct { @@ -173,35 +172,26 @@ func (s *symbol) TypeDefinition() protocol.Location { // It also accepts the includeDeclaration param from the client - if true, the declaration // of the symbol is included as a reference. func (s *symbol) References(includeDeclaration bool) []protocol.Location { - var references []protocol.Location - referenceableKind, ok := s.kind.(*referenceable) - if !ok && s.def != nil { - // If the symbol isn't referenceable itself, but has a referenceable definition, use the - // definition for the references. - referenceableKind, ok = s.def.kind.(*referenceable) - } - if ok { - for _, reference := range referenceableKind.references { - references = append(references, protocol.Location{ - URI: reference.file.uri, - Range: reference.Range(), - }) - } - } else { - // No referenceable kind; add the location of the symbol itself. - references = append(references, protocol.Location{ + declaration, ok := s.referenceDeclaration() + if !ok { + // No referenceable declaration; the symbol's own location is the only result. + return []protocol.Location{{ URI: s.file.uri, Range: s.Range(), + }} + } + var references []protocol.Location + for _, reference := range declaration.referenceSymbols() { + references = append(references, protocol.Location{ + URI: reference.file.uri, + Range: reference.Range(), }) } if includeDeclaration { - // Add the definition of the symbol to the list of references. - if s.def != nil { - references = append(references, protocol.Location{ - URI: s.def.file.uri, - Range: s.def.Range(), - }) - } + references = append(references, protocol.Location{ + URI: declaration.file.uri, + Range: declaration.Range(), + }) } return references } @@ -215,13 +205,8 @@ func (s *symbol) DocumentHighlights() []protocol.DocumentHighlight { return nil } - // Get the referenceable kind to find all references - referenceableKind, ok := s.kind.(*referenceable) - if !ok && s.def != nil { - // If the symbol isn't referenceable itself, but has a referenceable definition, use the - // definition for the references. - referenceableKind, ok = s.def.kind.(*referenceable) - } + // Get the referenceable declaration to find all references + declaration, ok := s.referenceDeclaration() if !ok { return nil } @@ -234,9 +219,9 @@ func (s *symbol) DocumentHighlights() []protocol.DocumentHighlight { } var highlights []protocol.DocumentHighlight - // Add all references in the same file - for _, reference := range referenceableKind.references { - if reference.file.uri == s.file.uri { + // Add all references in the same file. + if key, ok := newReferenceKeyForDeclaration(declaration); ok { + for _, reference := range s.file.lsp.referenceIndex.FileReferences(key, s.file.uri) { highlights = append(highlights, protocol.DocumentHighlight{ Range: reference.Range(), Kind: protocol.DocumentHighlightKindText, @@ -244,16 +229,10 @@ func (s *symbol) DocumentHighlights() []protocol.DocumentHighlight { } } - // Add the definition if it's in the same file - if s.def != nil && s.def.file.uri == s.file.uri { + // Add the declaration if it's in the same file + if declaration.file.uri == s.file.uri { highlights = append(highlights, protocol.DocumentHighlight{ - Range: s.def.Range(), - Kind: protocol.DocumentHighlightKindText, - }) - } else if s.def == nil { - // If there's no separate definition, the symbol itself is the definition - highlights = append(highlights, protocol.DocumentHighlight{ - Range: s.Range(), + Range: declaration.Range(), Kind: protocol.DocumentHighlightKindText, }) } @@ -448,6 +427,31 @@ func (s *symbol) Rename(newName string) (*protocol.WorkspaceEdit, error) { return &edits, nil } +// referenceDeclaration returns the referenceable symbol declaring what this symbol names: +// the symbol itself if it is referenceable, otherwise its resolved definition. Returns +// false if neither is, meaning the symbol cannot be referenced. +func (s *symbol) referenceDeclaration() (*symbol, bool) { + if _, ok := s.kind.(*referenceable); ok { + return s, true + } + if s.def != nil { + if _, ok := s.def.kind.(*referenceable); ok { + return s.def, true + } + } + return nil, false +} + +// referenceSymbols returns every symbol referencing this declaration symbol, across all +// indexed workspaces. +func (s *symbol) referenceSymbols() []*symbol { + key, ok := newReferenceKeyForDeclaration(s) + if !ok { + return nil + } + return s.file.lsp.referenceIndex.References(key) +} + // renameChangesForReferenceableSymbol is a helper for getting all rename changes for the // given referenceable symbol. func renameChangesForReferenceableSymbol(s *symbol, newName string) (map[protocol.DocumentURI][]protocol.TextEdit, error) { @@ -458,15 +462,10 @@ func renameChangesForReferenceableSymbol(s *symbol, newName string) (map[protoco NewText: newName, }}, } - // Get the referenceable kind to find all references - referenceableKind, ok := s.kind.(*referenceable) - if !ok && s.def != nil { - // If the symbol isn't referenceable itself, but has a referenceable definition, use the - // definition for the references. - referenceableKind, ok = s.def.kind.(*referenceable) - } + // Get the referenceable declaration to find all references + declaration, ok := s.referenceDeclaration() if ok { - for _, reference := range referenceableKind.references { + for _, reference := range declaration.referenceSymbols() { newText := newName // For option references (extension usages), preserve package qualification and parentheses. // e.g., if renaming "(subpkg.testing)" to "validated", result should be "(subpkg.validated)" diff --git a/private/buf/buflsp/testdata/references_dependency/buf.yaml b/private/buf/buflsp/testdata/references_dependency/buf.yaml new file mode 100644 index 0000000000..f74da98a3c --- /dev/null +++ b/private/buf/buflsp/testdata/references_dependency/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: . +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/private/buf/buflsp/testdata/references_dependency/first.proto b/private/buf/buflsp/testdata/references_dependency/first.proto new file mode 100644 index 0000000000..cb18a1f6fc --- /dev/null +++ b/private/buf/buflsp/testdata/references_dependency/first.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package referencesdep.v1; + +import "google/protobuf/timestamp.proto"; +import "shared.proto"; + +message First { + Shared shared = 1; + google.protobuf.Timestamp created_at = 2; + repeated Shared others = 3; +} diff --git a/private/buf/buflsp/testdata/references_dependency/second.proto b/private/buf/buflsp/testdata/references_dependency/second.proto new file mode 100644 index 0000000000..a3ff99d86c --- /dev/null +++ b/private/buf/buflsp/testdata/references_dependency/second.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package referencesdep.v1; + +import "google/protobuf/timestamp.proto"; +import "shared.proto"; + +message Second { + Shared shared = 1; + google.protobuf.Timestamp updated_at = 2; +} diff --git a/private/buf/buflsp/testdata/references_dependency/shared.proto b/private/buf/buflsp/testdata/references_dependency/shared.proto new file mode 100644 index 0000000000..592b8b9365 --- /dev/null +++ b/private/buf/buflsp/testdata/references_dependency/shared.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package referencesdep.v1; + +message Shared { + string id = 1; +}