on main help sign in
fix: address `nil` pointer dereference panics on lsp client methods (#2256)
9862d3aCopilot
internal/lsp/client.go+18
@@ -324,6 +324,9 @@ type OpenFileInfo struct {
324// HandlesFile checks if this LSP client handles the given file based on its
324// HandlesFile checks if this LSP client handles the given file based on its
325// extension and whether it's within the working directory.
325// extension and whether it's within the working directory.
326func (c *Client) HandlesFile(path string) bool {
326func (c *Client) HandlesFile(path string) bool {
327+ if c == nil {
328+ return false
329+ }
327 if !fsext.HasPrefix(path, c.cwd) {
330 if !fsext.HasPrefix(path, c.cwd) {
328 slog.Debug("File outside workspace", "name", c.name, "file", path, "workDir", c.cwd)
331 slog.Debug("File outside workspace", "name", c.name, "file", path, "workDir", c.cwd)
329 return false
332 return false
@@ -364,6 +367,9 @@ func (c *Client) OpenFile(ctx context.Context, filepath string) error {
364
367
365// NotifyChange notifies the server about a file change.
368// NotifyChange notifies the server about a file change.
366func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
369func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
370+ if c == nil {
371+ return nil
372+ }
367 uri := string(protocol.URIFromPath(filepath))
373 uri := string(protocol.URIFromPath(filepath))
368
374
369 content, err := os.ReadFile(filepath)
375 content, err := os.ReadFile(filepath)
@@ -420,12 +426,18 @@ func (c *Client) GetFileDiagnostics(uri protocol.DocumentURI) []protocol.Diagnos
420
426
421// GetDiagnostics returns all diagnostics for all files.
427// GetDiagnostics returns all diagnostics for all files.
422func (c *Client) GetDiagnostics() map[protocol.DocumentURI][]protocol.Diagnostic {
428func (c *Client) GetDiagnostics() map[protocol.DocumentURI][]protocol.Diagnostic {
429+ if c == nil {
430+ return nil
431+ }
423 return c.diagnostics.Copy()
432 return c.diagnostics.Copy()
424}
433}
425
434
426// GetDiagnosticCounts returns cached diagnostic counts by severity.
435// GetDiagnosticCounts returns cached diagnostic counts by severity.
427// Uses the VersionedMap version to avoid recomputing on every call.
436// Uses the VersionedMap version to avoid recomputing on every call.
428func (c *Client) GetDiagnosticCounts() DiagnosticCounts {
437func (c *Client) GetDiagnosticCounts() DiagnosticCounts {
438+ if c == nil {
439+ return DiagnosticCounts{}
440+ }
429 currentVersion := c.diagnostics.Version()
441 currentVersion := c.diagnostics.Version()
430
442
431 c.diagCountsMu.Lock()
443 c.diagCountsMu.Lock()
@@ -459,6 +471,9 @@ func (c *Client) GetDiagnosticCounts() DiagnosticCounts {
459
471
460// OpenFileOnDemand opens a file only if it's not already open.
472// OpenFileOnDemand opens a file only if it's not already open.
461func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error {
473func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error {
474+ if c == nil {
475+ return nil
476+ }
462 // Check if the file is already open
477 // Check if the file is already open
463 if c.IsFileOpen(filepath) {
478 if c.IsFileOpen(filepath) {
464 return nil
479 return nil
@@ -501,6 +516,9 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) {
501
516
502// WaitForDiagnostics waits until diagnostics change or the timeout is reached.
517// WaitForDiagnostics waits until diagnostics change or the timeout is reached.
503func (c *Client) WaitForDiagnostics(ctx context.Context, d time.Duration) {
518func (c *Client) WaitForDiagnostics(ctx context.Context, d time.Duration) {
519+ if c == nil {
520+ return
521+ }
504 ticker := time.NewTicker(200 * time.Millisecond)
522 ticker := time.NewTicker(200 * time.Millisecond)
505 defer ticker.Stop()
523 defer ticker.Stop()
506 timeout := time.After(d)
524 timeout := time.After(d)
internal/lsp/client_test.go+15
@@ -3,9 +3,11 @@ package lsp
3import (
3import (
4 "context"
4 "context"
5 "testing"
5 "testing"
6+ "time"
6
7
7 "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/env"
9 "github.com/charmbracelet/crush/internal/env"
10+ "github.com/stretchr/testify/require"
9)
11)
10
12
11func TestClient(t *testing.T) {
13func TestClient(t *testing.T) {
@@ -55,3 +57,16 @@ func TestClient(t *testing.T) {
55 t.Logf("Close failed as expected with dummy command: %v", err)
57 t.Logf("Close failed as expected with dummy command: %v", err)
56 }
58 }
57}
59}
60+
61+func TestNilClient(t *testing.T) {
62+ t.Parallel()
63+
64+ var c *Client
65+
66+ require.False(t, c.HandlesFile("/some/file.go"))
67+ require.Equal(t, DiagnosticCounts{}, c.GetDiagnosticCounts())
68+ require.Nil(t, c.GetDiagnostics())
69+ require.Nil(t, c.OpenFileOnDemand(context.Background(), "/some/file.go"))
70+ require.Nil(t, c.NotifyChange(context.Background(), "/some/file.go"))
71+ c.WaitForDiagnostics(context.Background(), time.Second)
72+}