diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-16 04:03:44 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-16 04:03:44 +0200 |
| commit | 8f31040cc388943601cfd8a026ea85f0790e66c2 (patch) | |
| tree | 682ab9d8e0cf698f754866ec63e4eb955eb684bd /internal | |
| parent | 030ce454f330871a6be729d7ca8c6ac33e1bbbb5 (diff) | |
Add bounds checks to extractRangeText and split into helper functions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/lsp/extract_range_text_test.go | 83 | ||||
| -rw-r--r-- | internal/lsp/handlers_utils.go | 57 |
2 files changed, 126 insertions, 14 deletions
diff --git a/internal/lsp/extract_range_text_test.go b/internal/lsp/extract_range_text_test.go new file mode 100644 index 0000000..1fe29ae --- /dev/null +++ b/internal/lsp/extract_range_text_test.go @@ -0,0 +1,83 @@ +package lsp + +import "testing" + +func TestExtractRangeText_NilDocument(t *testing.T) { + got := extractRangeText(nil, Range{}) + if got != "" { + t.Fatalf("expected empty, got %q", got) + } +} + +func TestExtractRangeText_EmptyLines(t *testing.T) { + d := &document{lines: []string{}} + got := extractRangeText(d, Range{Start: Position{0, 0}, End: Position{0, 5}}) + if got != "" { + t.Fatalf("expected empty, got %q", got) + } +} + +func TestExtractRangeText_NegativeStartLine(t *testing.T) { + d := &document{lines: []string{"hello"}} + got := extractRangeText(d, Range{Start: Position{-1, 0}, End: Position{0, 3}}) + if got != "" { + t.Fatalf("expected empty, got %q", got) + } +} + +func TestExtractRangeText_NegativeEndLine(t *testing.T) { + d := &document{lines: []string{"hello"}} + got := extractRangeText(d, Range{Start: Position{0, 0}, End: Position{-1, 3}}) + if got != "" { + t.Fatalf("expected empty, got %q", got) + } +} + +func TestExtractRangeText_StartLineBeyondEnd(t *testing.T) { + d := &document{lines: []string{"hello", "world"}} + got := extractRangeText(d, Range{Start: Position{5, 0}, End: Position{6, 0}}) + if got != "" { + t.Fatalf("expected empty, got %q", got) + } +} + +func TestExtractRangeText_EndLineBeyondEnd(t *testing.T) { + // End line is clamped to the last valid line. + d := &document{lines: []string{"hello", "world"}} + got := extractRangeText(d, Range{Start: Position{0, 0}, End: Position{10, 5}}) + if got != "hello\nworld" { + t.Fatalf("expected clamped result, got %q", got) + } +} + +func TestExtractRangeText_StartAfterEnd(t *testing.T) { + d := &document{lines: []string{"aaa", "bbb"}} + got := extractRangeText(d, Range{Start: Position{1, 0}, End: Position{0, 2}}) + if got != "" { + t.Fatalf("expected empty, got %q", got) + } +} + +func TestExtractRangeText_SingleLine(t *testing.T) { + d := &document{lines: []string{"hello world"}} + got := extractRangeText(d, Range{Start: Position{0, 6}, End: Position{0, 11}}) + if got != "world" { + t.Fatalf("expected %q, got %q", "world", got) + } +} + +func TestExtractRangeText_SingleLineNegativeChar(t *testing.T) { + d := &document{lines: []string{"hello"}} + got := extractRangeText(d, Range{Start: Position{0, -3}, End: Position{0, 3}}) + if got != "hel" { + t.Fatalf("expected %q, got %q", "hel", got) + } +} + +func TestExtractRangeText_MultiLine(t *testing.T) { + d := &document{lines: []string{"aaa", "bbb", "ccc"}} + got := extractRangeText(d, Range{Start: Position{0, 1}, End: Position{2, 2}}) + if got != "aa\nbbb\ncc" { + t.Fatalf("expected %q, got %q", "aa\nbbb\ncc", got) + } +} diff --git a/internal/lsp/handlers_utils.go b/internal/lsp/handlers_utils.go index 4a5bf4a..620b3a9 100644 --- a/internal/lsp/handlers_utils.go +++ b/internal/lsp/handlers_utils.go @@ -550,23 +550,52 @@ func labelForCompletion(cleaned, filter string) string { } // extractRangeText returns the exact text within the given document range. +// It performs bounds checks on line indices and character offsets, returning +// an empty string when the range is invalid (e.g. negative lines, out-of-bounds +// lines, or an empty document). func extractRangeText(d *document, r Range) string { + if d == nil || len(d.lines) == 0 { + return "" + } + // Clamp line indices to valid bounds. + if r.Start.Line < 0 || r.End.Line < 0 || r.Start.Line >= len(d.lines) { + return "" + } + if r.End.Line >= len(d.lines) { + r.End.Line = len(d.lines) - 1 + r.End.Character = len(d.lines[r.End.Line]) + } + if r.Start.Line > r.End.Line { + return "" + } + if r.Start.Line == r.End.Line { - line := d.lines[r.Start.Line] - if r.Start.Character < 0 { - r.Start.Character = 0 - } - if r.End.Character > len(line) { - r.End.Character = len(line) - } - if r.Start.Character > r.End.Character { - return "" - } - return line[r.Start.Character:r.End.Character] + return extractSingleLineRange(d.lines[r.Start.Line], r) + } + return extractMultiLineRange(d.lines, r) +} + +// extractSingleLineRange handles the case where start and end are on the same line. +// Character offsets are clamped to the line length. +func extractSingleLineRange(line string, r Range) string { + if r.Start.Character < 0 { + r.Start.Character = 0 } + if r.End.Character > len(line) { + r.End.Character = len(line) + } + if r.Start.Character > r.End.Character { + return "" + } + return line[r.Start.Character:r.End.Character] +} + +// extractMultiLineRange handles ranges spanning multiple lines, clamping +// character offsets on the first and last lines. +func extractMultiLineRange(lines []string, r Range) string { var b strings.Builder // first line - first := d.lines[r.Start.Line] + first := lines[r.Start.Line] if r.Start.Character < 0 { r.Start.Character = 0 } @@ -577,13 +606,13 @@ func extractRangeText(d *document, r Range) string { b.WriteString("\n") // middle lines for i := r.Start.Line + 1; i < r.End.Line; i++ { - b.WriteString(d.lines[i]) + b.WriteString(lines[i]) if i+1 <= r.End.Line { b.WriteString("\n") } } // last line - last := d.lines[r.End.Line] + last := lines[r.End.Line] if r.End.Character < 0 { r.End.Character = 0 } |
