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..bd5c86c954 100644 --- a/go.sum +++ b/go.sum @@ -38,12 +38,12 @@ 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= 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..feb8059dd8 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, @@ -50,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,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 + return diagnostic } 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..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()), @@ -1139,7 +1129,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 +1138,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; +}