From c4f9a5ea2af656fcf4ac66c05a0fb88b17f45be7 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Fri, 30 Jan 2026 11:38:40 -0500 Subject: [PATCH 1/3] Add tags to diagnostics Also removes the diagnostic.Data from the new diagnostics, which AFAICS is unused; we can save ourselves the json marshaling. As a part of this, added a first pass at adding tests for diagnostics; we don't currently have any. These are a bit different because we haven't implemented [PullDiagnostics][1] (we can add this, but we'd need a custom implementation, or wait until [go.lsp.dev upgrades to 3.17][2]). Instead, we intercept the notifications from the server. Lastly, since we don't actually want the tests to wait around, adds synctest and only runs the tests on Go 1.25 for now (1.26 should be released next month and we'll be able to upgrade to 1.25 as our minimum). This is in preparation to eventually landing relatedInformation (other spans) in diagnostics, from bufbuild/protocompile#659. Also pulls in the latest protocompile@main to bring in the new deprecated tag. [1]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics [2]: go-language-server/protocol#52 --- go.mod | 2 +- go.sum | 3 + private/buf/buflsp/buflsp_test.go | 34 +- private/buf/buflsp/diagnostic.go | 35 +- private/buf/buflsp/diagnostics_test.go | 361 ++++++++++++++++++ private/buf/buflsp/file.go | 10 +- private/buf/buflsp/symbol.go | 6 +- .../buf/buflsp/testdata/diagnostics/buf.yaml | 6 + .../testdata/diagnostics/syntax_error.proto | 9 + .../testdata/diagnostics/unused_import.proto | 10 + .../buflsp/testdata/diagnostics/valid.proto | 9 + 11 files changed, 435 insertions(+), 50 deletions(-) create mode 100644 private/buf/buflsp/diagnostics_test.go create mode 100644 private/buf/buflsp/testdata/diagnostics/buf.yaml create mode 100644 private/buf/buflsp/testdata/diagnostics/syntax_error.proto create mode 100644 private/buf/buflsp/testdata/diagnostics/unused_import.proto create mode 100644 private/buf/buflsp/testdata/diagnostics/valid.proto diff --git a/go.mod b/go.mod index c3f9ccf6c0..a31e2cccc9 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( buf.build/go/standard v0.1.0 connectrpc.com/connect v1.19.1 connectrpc.com/otelconnect v0.9.0 - github.com/bufbuild/protocompile v0.14.2-0.20260121154354-91940cd2bca3 + github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 github.com/cli/browser v1.3.0 github.com/docker/docker v28.5.2+incompatible diff --git a/go.sum b/go.sum index 8daf2c5305..ce24117a00 100644 --- a/go.sum +++ b/go.sum @@ -40,10 +40,13 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/bmatcuk/doublestar/v4 v4.9.2 h1:b0mc6WyRSYLjzofB2v/0cuDUZ+MqoGyH3r0dVij35GI= github.com/bmatcuk/doublestar/v4 v4.9.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/bufbuild/protocompile v0.14.2-0.20260121154354-91940cd2bca3 h1:7wRdWMBC4bFiS66NRJtMWRimxYik53CKtcU8KPP1EMg= github.com/bufbuild/protocompile v0.14.2-0.20260121154354-91940cd2bca3/go.mod h1:H51HpPHpeLddOIg/OZAuZsPmlKvP8pyXKXAkOUin9ZM= +github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co= +github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE= github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU= github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= diff --git a/private/buf/buflsp/buflsp_test.go b/private/buf/buflsp/buflsp_test.go index e666d7f815..4fe7484315 100644 --- a/private/buf/buflsp/buflsp_test.go +++ b/private/buf/buflsp/buflsp_test.go @@ -121,26 +121,20 @@ func setupLSPServer( stream := jsonrpc2.NewStream(serverConn) - go func() { - conn, err := buflsp.Serve( - ctx, - "test", - wktBucket, - appextContainer, - controller, - wasmRuntime, - stream, - queryExecutor, - ) - if err != nil { - t.Errorf("Failed to start server: %v", err) - return - } - t.Cleanup(func() { - require.NoError(t, conn.Close()) - }) - <-ctx.Done() - }() + conn, err := buflsp.Serve( + ctx, + "test", + wktBucket, + appextContainer, + controller, + wasmRuntime, + stream, + queryExecutor, + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, conn.Close()) + }) clientStream := jsonrpc2.NewStream(clientConn) clientJSONConn := jsonrpc2.NewConn(clientStream) diff --git a/private/buf/buflsp/diagnostic.go b/private/buf/buflsp/diagnostic.go index ee24272f7e..5650e40ccc 100644 --- a/private/buf/buflsp/diagnostic.go +++ b/private/buf/buflsp/diagnostic.go @@ -17,10 +17,8 @@ package buflsp import ( - "encoding/json" - "strings" - "github.com/bufbuild/protocompile/experimental/report" + "github.com/bufbuild/protocompile/experimental/report/tags" "github.com/bufbuild/protocompile/experimental/source/length" "go.lsp.dev/protocol" ) @@ -30,14 +28,6 @@ import ( // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position const positionalEncoding = length.UTF16 -// diagnosticData is a structure to hold the [report.Diagnostic] notes, help, and debug -// messages, to marshal into JSON for the [protocol.Diagnostic].Data field. -type diagnosticData struct { - Notes string `json:"notes,omitempty"` - Help string `json:"help,omitempty"` - Debug string `json:"debug,omitempty"` -} - // reportLevelToDiagnosticSeverity is a mapping of [report.Level] to [protocol.DiagnosticSeverity]. var reportLevelToDiagnosticSeverity = map[report.Level]protocol.DiagnosticSeverity{ report.ICE: protocol.DiagnosticSeverityError, @@ -70,20 +60,15 @@ func reportDiagnosticToProtocolDiagnostic( }, } } - data := diagnosticData{ - Notes: strings.Join(reportDiagnostic.Notes(), "\n"), - Help: strings.Join(reportDiagnostic.Help(), "\n"), - Debug: strings.Join(reportDiagnostic.Debug(), "\n"), - } - bytes, err := json.Marshal(data) - if err != nil { - return protocol.Diagnostic{}, err - } - if bytes != nil { - // We serialize the bytes into a string before providing the structure to diagnostic.Data - // because diagnostic.Data is an interface{}, which is treated as a JSON "any", which - // will not cleanly deserialize. - diagnostic.Data = string(bytes) + switch reportDiagnostic.Tag() { + case tags.UnusedImport: + diagnostic.Tags = []protocol.DiagnosticTag{ + protocol.DiagnosticTagUnnecessary, + } + case tags.Deprecated: + diagnostic.Tags = []protocol.DiagnosticTag{ + protocol.DiagnosticTagDeprecated, + } } return diagnostic, nil } diff --git a/private/buf/buflsp/diagnostics_test.go b/private/buf/buflsp/diagnostics_test.go new file mode 100644 index 0000000000..041e503edc --- /dev/null +++ b/private/buf/buflsp/diagnostics_test.go @@ -0,0 +1,361 @@ +// Copyright 2020-2025 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. + +//go:build go1.25 + +package buflsp_test + +import ( + "context" + "encoding/json" + "net" + "net/http" + "os" + "path/filepath" + "sync" + "testing" + "testing/synctest" + "time" + + "buf.build/go/app" + "buf.build/go/app/appext" + "github.com/bufbuild/buf/private/buf/bufctl" + "github.com/bufbuild/buf/private/buf/buflsp" + "github.com/bufbuild/buf/private/buf/bufwkt/bufwktstore" + "github.com/bufbuild/buf/private/bufpkg/bufmodule" + "github.com/bufbuild/buf/private/bufpkg/bufplugin" + "github.com/bufbuild/buf/private/bufpkg/bufpolicy" + "github.com/bufbuild/buf/private/pkg/git" + "github.com/bufbuild/buf/private/pkg/httpauth" + "github.com/bufbuild/buf/private/pkg/slogtestext" + "github.com/bufbuild/buf/private/pkg/storage/storageos" + "github.com/bufbuild/buf/private/pkg/wasm" + "github.com/bufbuild/protocompile/experimental/incremental" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.lsp.dev/jsonrpc2" + "go.lsp.dev/protocol" + "go.lsp.dev/uri" +) + +// setupLSPServerWithDiagnostics creates and initializes an LSP server for testing with diagnostic capture. +func setupLSPServerWithDiagnostics( + t *testing.T, + testProtoPath string, +) (jsonrpc2.Conn, protocol.URI, *diagnosticsCapture) { + t.Helper() + + ctx := t.Context() + + logger := slogtestext.NewLogger(t, slogtestext.WithLogLevel(appext.LogLevelInfo)) + + appContainer, err := app.NewContainerForOS() + require.NoError(t, err) + + nameContainer, err := appext.NewNameContainer(appContainer, "buf-test") + require.NoError(t, err) + appextContainer := appext.NewContainer(nameContainer, logger) + + graphProvider := bufmodule.NopGraphProvider + moduleDataProvider := bufmodule.NopModuleDataProvider + commitProvider := bufmodule.NopCommitProvider + pluginKeyProvider := bufplugin.NopPluginKeyProvider + pluginDataProvider := bufplugin.NopPluginDataProvider + policyKeyProvider := bufpolicy.NopPolicyKeyProvider + policyDataProvider := bufpolicy.NopPolicyDataProvider + + tmpDir := t.TempDir() + storageBucket, err := storageos.NewProvider().NewReadWriteBucket(tmpDir) + require.NoError(t, err) + + wktStore := bufwktstore.NewStore(logger, storageBucket) + + controller, err := bufctl.NewController( + logger, + appContainer, + graphProvider, + nopModuleKeyProvider{}, + moduleDataProvider, + commitProvider, + pluginKeyProvider, + pluginDataProvider, + policyKeyProvider, + policyDataProvider, + wktStore, + &http.Client{}, + httpauth.NewNopAuthenticator(), + git.ClonerOptions{}, + ) + require.NoError(t, err) + + wktBucket, err := wktStore.GetBucket(ctx) + require.NoError(t, err) + + wasmRuntime, err := wasm.NewRuntime(ctx) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, wasmRuntime.Close(ctx)) + }) + + queryExecutor := incremental.New() + + serverConn, clientConn := net.Pipe() + t.Cleanup(func() { + require.NoError(t, serverConn.Close()) + require.NoError(t, clientConn.Close()) + }) + + stream := jsonrpc2.NewStream(serverConn) + + conn, err := buflsp.Serve( + ctx, + "test", + wktBucket, + appextContainer, + controller, + wasmRuntime, + stream, + queryExecutor, + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, conn.Close()) + }) + + capture := newDiagnosticsCapture() + + clientStream := jsonrpc2.NewStream(clientConn) + clientJSONConn := jsonrpc2.NewConn(clientStream) + clientJSONConn.Go(ctx, jsonrpc2.AsyncHandler(capture.handle)) + t.Cleanup(func() { + require.NoError(t, clientJSONConn.Close()) + }) + + testWorkspaceDir := filepath.Dir(testProtoPath) + testURI := uri.New(testProtoPath) + var initResult protocol.InitializeResult + _, initErr := clientJSONConn.Call(ctx, protocol.MethodInitialize, &protocol.InitializeParams{ + RootURI: uri.New(testWorkspaceDir), + Capabilities: protocol.ClientCapabilities{ + TextDocument: &protocol.TextDocumentClientCapabilities{}, + }, + }, &initResult) + require.NoError(t, initErr) + + err = clientJSONConn.Notify(ctx, protocol.MethodInitialized, &protocol.InitializedParams{}) + require.NoError(t, err) + + testProtoContent, err := os.ReadFile(testProtoPath) + require.NoError(t, err) + + err = clientJSONConn.Notify(ctx, protocol.MethodTextDocumentDidOpen, &protocol.DidOpenTextDocumentParams{ + TextDocument: protocol.TextDocumentItem{ + URI: testURI, + LanguageID: "protobuf", + Version: 1, + Text: string(testProtoContent), + }, + }) + require.NoError(t, err) + + return clientJSONConn, testURI, capture +} + +// TestDiagnostics tests various diagnostic scenarios published by the LSP server. +// Each subtest uses synctest to provide deterministic timing for async diagnostics. +func TestDiagnostics(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + protoFile string + expectedDiagnostics []protocol.Diagnostic + }{ + { + name: "valid_proto_no_diagnostics", + protoFile: "testdata/diagnostics/valid.proto", + expectedDiagnostics: []protocol.Diagnostic{}, + }, + { + name: "syntax_error_diagnostic", + protoFile: "testdata/diagnostics/syntax_error.proto", + expectedDiagnostics: []protocol.Diagnostic{ + { + Range: protocol.Range{ + Start: protocol.Position{Line: 8, Character: 0}, + End: protocol.Position{Line: 8, Character: 0}, + }, + Severity: protocol.DiagnosticSeverityError, + Source: "buf-lsp", + Message: "syntax error: expecting ';'", + }, + }, + }, + { + name: "unused_import_diagnostic_with_tag", + protoFile: "testdata/diagnostics/unused_import.proto", + expectedDiagnostics: []protocol.Diagnostic{ + { + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 0}, + End: protocol.Position{Line: 4, Character: 41}, + }, + Severity: protocol.DiagnosticSeverityWarning, + Code: "IMPORT_USED", + CodeDescription: &protocol.CodeDescription{ + Href: "https://buf.build/docs/lint/rules/#import_used", + }, + Source: "buf lint", + Message: `Import "google/protobuf/timestamp.proto" is unused.`, + Tags: []protocol.DiagnosticTag{protocol.DiagnosticTagUnnecessary}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + synctest.Test(t, func(t *testing.T) { + protoPath, err := filepath.Abs(tt.protoFile) + require.NoError(t, err) + + _, testURI, capture := setupLSPServerWithDiagnostics(t, protoPath) + + // Wait for diagnostics to be published + timeout := 5 * time.Second + if len(tt.expectedDiagnostics) > 0 { + timeout = 10 * time.Second // Lint checks take longer + } + diagnostics := capture.wait(t, testURI, timeout, func(p *protocol.PublishDiagnosticsParams) bool { + return len(p.Diagnostics) >= len(tt.expectedDiagnostics) + }) + + require.NotNil(t, diagnostics, "expected diagnostics to be published") + assert.Equal(t, testURI, diagnostics.URI) + + // Check that we have the expected number of diagnostics + require.Len(t, diagnostics.Diagnostics, len(tt.expectedDiagnostics), + "expected %d diagnostic(s), got %d", len(tt.expectedDiagnostics), len(diagnostics.Diagnostics)) + + // Compare each diagnostic directly + for i, expected := range tt.expectedDiagnostics { + actual := diagnostics.Diagnostics[i] + assert.Equal(t, expected, actual, "diagnostic %d mismatch", i) + } + }) + }) + } +} + +// TestDiagnosticsUpdate tests that diagnostics are updated when file content changes. +// Uses synctest to provide deterministic timing for the async diagnostic updates. +func TestDiagnosticsUpdate(t *testing.T) { + t.Parallel() + + synctest.Test(t, func(t *testing.T) { + protoPath, err := filepath.Abs("testdata/diagnostics/valid.proto") + require.NoError(t, err) + + clientJSONConn, testURI, capture := setupLSPServerWithDiagnostics(t, protoPath) + + ctx := t.Context() + + // Wait for initial diagnostics (should be empty for valid file) + initialDiagnostics := capture.wait(t, testURI, 5*time.Second, func(p *protocol.PublishDiagnosticsParams) bool { + return true // Accept any diagnostics + }) + require.NotNil(t, initialDiagnostics) + assert.Empty(t, initialDiagnostics.Diagnostics, "expected no initial diagnostics for valid file") + + // Update the file with invalid content (missing semicolon) + invalidContent := `syntax = "proto3"; + +package diagnostics.v1; + +message TestMessage { + string name = 1 + // Missing semicolon above +} +` + + err = clientJSONConn.Notify(ctx, protocol.MethodTextDocumentDidChange, &protocol.DidChangeTextDocumentParams{ + TextDocument: protocol.VersionedTextDocumentIdentifier{ + TextDocumentIdentifier: protocol.TextDocumentIdentifier{ + URI: testURI, + }, + Version: 2, + }, + ContentChanges: []protocol.TextDocumentContentChangeEvent{ + { + Text: invalidContent, + }, + }, + }) + require.NoError(t, err) + + // Wait for updated diagnostics with version 2 and at least one error + updatedDiagnostics := capture.wait(t, testURI, 5*time.Second, func(p *protocol.PublishDiagnosticsParams) bool { + return p.Version == 2 && len(p.Diagnostics) > 0 + }) + require.NotNil(t, updatedDiagnostics) + assert.Equal(t, uint32(2), updatedDiagnostics.Version, "expected diagnostics version to match file version") + assert.NotEmpty(t, updatedDiagnostics.Diagnostics, "expected diagnostics after introducing syntax error") + + if len(updatedDiagnostics.Diagnostics) > 0 { + assert.Equal(t, protocol.DiagnosticSeverityError, updatedDiagnostics.Diagnostics[0].Severity) + } + }) +} + +// diagnosticsCapture captures publishDiagnostics notifications from the LSP server. +type diagnosticsCapture struct { + mu sync.Mutex + diagnostics map[protocol.URI]*protocol.PublishDiagnosticsParams +} + +func newDiagnosticsCapture() *diagnosticsCapture { + return &diagnosticsCapture{ + diagnostics: make(map[protocol.URI]*protocol.PublishDiagnosticsParams), + } +} + +func (dc *diagnosticsCapture) handle(_ context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { + if req.Method() == protocol.MethodTextDocumentPublishDiagnostics { + var params protocol.PublishDiagnosticsParams + if err := json.Unmarshal(req.Params(), ¶ms); err == nil { + dc.mu.Lock() + dc.diagnostics[params.URI] = ¶ms + dc.mu.Unlock() + } + } + return reply(context.Background(), nil, nil) +} + +// wait polls for diagnostics matching the predicate. +func (dc *diagnosticsCapture) wait(t *testing.T, uri protocol.URI, timeout time.Duration, pred func(*protocol.PublishDiagnosticsParams) bool) *protocol.PublishDiagnosticsParams { + t.Helper() + + require.Eventually(t, func() bool { + dc.mu.Lock() + params := dc.diagnostics[uri] + dc.mu.Unlock() + return params != nil && pred(params) + }, timeout, 50*time.Millisecond, "timeout waiting for diagnostics matching predicate") + + dc.mu.Lock() + defer dc.mu.Unlock() + return dc.diagnostics[uri] +} diff --git a/private/buf/buflsp/file.go b/private/buf/buflsp/file.go index 9eb950f1d9..2c5fceab8e 100644 --- a/private/buf/buflsp/file.go +++ b/private/buf/buflsp/file.go @@ -1139,7 +1139,7 @@ func (f *file) appendAnnotations(source string, annotations []bufanalysis.FileAn startLocation := f.file.InverseLocation(annotation.StartLine(), annotation.StartColumn(), positionalEncoding) endLocation := f.file.InverseLocation(annotation.EndLine(), annotation.EndColumn(), positionalEncoding) protocolRange := reportLocationsToProtocolRange(startLocation, endLocation) - f.diagnostics = append(f.diagnostics, protocol.Diagnostic{ + diagnostic := protocol.Diagnostic{ Range: protocolRange, Code: annotation.Type(), CodeDescription: &protocol.CodeDescription{ @@ -1148,7 +1148,13 @@ func (f *file) appendAnnotations(source string, annotations []bufanalysis.FileAn Severity: protocol.DiagnosticSeverityWarning, Source: source, Message: annotation.Message(), - }) + } + if annotation.Type() == "IMPORT_USED" { + diagnostic.Tags = []protocol.DiagnosticTag{ + protocol.DiagnosticTagUnnecessary, + } + } + f.diagnostics = append(f.diagnostics, diagnostic) } } diff --git a/private/buf/buflsp/symbol.go b/private/buf/buflsp/symbol.go index cf93de24fa..9616800c56 100644 --- a/private/buf/buflsp/symbol.go +++ b/private/buf/buflsp/symbol.go @@ -364,8 +364,10 @@ func (s *symbol) GetSymbolInformation() protocol.SymbolInformation { Kind: kind, Location: location, ContainerName: containerName, - // TODO: Use Tags with a protocol.CompletionItemTagDeprecated if the client supports tags. - Deprecated: isDeprecated, + Deprecated: isDeprecated, + Tags: []protocol.SymbolTag{ + protocol.SymbolTagDeprecated, + }, } } diff --git a/private/buf/buflsp/testdata/diagnostics/buf.yaml b/private/buf/buflsp/testdata/diagnostics/buf.yaml new file mode 100644 index 0000000000..6fc5e4bb71 --- /dev/null +++ b/private/buf/buflsp/testdata/diagnostics/buf.yaml @@ -0,0 +1,6 @@ +version: v2 +lint: + use: + - STANDARD + except: + - PACKAGE_DIRECTORY_MATCH diff --git a/private/buf/buflsp/testdata/diagnostics/syntax_error.proto b/private/buf/buflsp/testdata/diagnostics/syntax_error.proto new file mode 100644 index 0000000000..0c8d8adf66 --- /dev/null +++ b/private/buf/buflsp/testdata/diagnostics/syntax_error.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package diagnostics.v1; + +// This file has a syntax error for testing diagnostics +message TestMessage { + string name = 1 + // Missing semicolon above +} diff --git a/private/buf/buflsp/testdata/diagnostics/unused_import.proto b/private/buf/buflsp/testdata/diagnostics/unused_import.proto new file mode 100644 index 0000000000..a37777ae48 --- /dev/null +++ b/private/buf/buflsp/testdata/diagnostics/unused_import.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package diagnostics.v1; + +import "google/protobuf/timestamp.proto"; + +// timestamp is imported but not used - should trigger unused import diagnostic +message TestMessage { + string name = 1; +} diff --git a/private/buf/buflsp/testdata/diagnostics/valid.proto b/private/buf/buflsp/testdata/diagnostics/valid.proto new file mode 100644 index 0000000000..012a8c0f42 --- /dev/null +++ b/private/buf/buflsp/testdata/diagnostics/valid.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package diagnostics.v1; + +// A valid proto file with no issues +message TestMessage { + string name = 1; + int32 age = 2; +} From e5a017ce58509227b1ca2270c4977df86bc730e6 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Fri, 30 Jan 2026 15:10:41 -0500 Subject: [PATCH 2/3] Run `go mod tidy` --- go.sum | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/go.sum b/go.sum index ce24117a00..bd5c86c954 100644 --- a/go.sum +++ b/go.sum @@ -38,13 +38,10 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -github.com/bmatcuk/doublestar/v4 v4.9.2 h1:b0mc6WyRSYLjzofB2v/0cuDUZ+MqoGyH3r0dVij35GI= -github.com/bmatcuk/doublestar/v4 v4.9.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= -github.com/bufbuild/protocompile v0.14.2-0.20260121154354-91940cd2bca3 h1:7wRdWMBC4bFiS66NRJtMWRimxYik53CKtcU8KPP1EMg= -github.com/bufbuild/protocompile v0.14.2-0.20260121154354-91940cd2bca3/go.mod h1:H51HpPHpeLddOIg/OZAuZsPmlKvP8pyXKXAkOUin9ZM= github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co= github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE= github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU= From 057726f3bb773de5ec99e23aa8f3ccc588c8d782 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Fri, 30 Jan 2026 16:08:59 -0500 Subject: [PATCH 3/3] Drop error return; simplify callsite --- private/buf/buflsp/diagnostic.go | 4 ++-- private/buf/buflsp/file.go | 12 +----------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/private/buf/buflsp/diagnostic.go b/private/buf/buflsp/diagnostic.go index 5650e40ccc..feb8059dd8 100644 --- a/private/buf/buflsp/diagnostic.go +++ b/private/buf/buflsp/diagnostic.go @@ -40,7 +40,7 @@ var reportLevelToDiagnosticSeverity = map[report.Level]protocol.DiagnosticSeveri // corresponding [protocol.Diagnostic]. func reportDiagnosticToProtocolDiagnostic( reportDiagnostic report.Diagnostic, -) (protocol.Diagnostic, error) { +) protocol.Diagnostic { diagnostic := protocol.Diagnostic{ Source: serverName, Severity: reportLevelToDiagnosticSeverity[reportDiagnostic.Level()], @@ -70,5 +70,5 @@ func reportDiagnosticToProtocolDiagnostic( protocol.DiagnosticTagDeprecated, } } - return diagnostic, nil + return diagnostic } diff --git a/private/buf/buflsp/file.go b/private/buf/buflsp/file.go index 2c5fceab8e..544689a83c 100644 --- a/private/buf/buflsp/file.go +++ b/private/buf/buflsp/file.go @@ -290,17 +290,7 @@ func (f *file) RefreshIR(ctx context.Context) { fileDiagnostics := xslices.Filter(diagnosticReport.Diagnostics, func(d report.Diagnostic) bool { return d.Primary().Path() == f.objectInfo.Path() }) - diagnostics, err := xslices.MapError( - fileDiagnostics, - reportDiagnosticToProtocolDiagnostic, - ) - if err != nil { - f.lsp.logger.Error( - "failed to parse report diagnostics", - xslog.ErrorAttr(err), - ) - } - f.diagnostics = diagnostics + f.diagnostics = xslices.Map(fileDiagnostics, reportDiagnosticToProtocolDiagnostic) f.lsp.logger.DebugContext( ctx, "ir diagnostic(s)", slog.String("uri", f.uri.Filename()),