internal/diff: Apply: validate inputs
Apply now checks that its edits are valid
(not out of bounds or overlapping),
and reports an error if not.
It also sorts them, if necessary, using (start, end)
as the key, to ensure that insertions (end=start)
are ordered before deletions at the same point
(but without changing the relative order of insertions).
Two other implementations of the diff.Apply algorithm
have been eliminated. (One of them failed to sort edits,
requiring the protocol sender to do so; that burden
is now gone.)
Change-Id: Ia76e485e6869db4a165835c3312fd14bc7d43db2
Reviewed-on: https://go-review.googlesource.com/c/tools/+/439278
Auto-Submit: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
Run-TryBot: Alan Donovan <adonovan@google.com>
diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go
index bc25b9f..14140be 100644
--- a/go/analysis/analysistest/analysistest.go
+++ b/go/analysis/analysistest/analysistest.go
@@ -183,7 +183,11 @@
for _, vf := range ar.Files {
if vf.Name == sf {
found = true
- out := diff.Apply(string(orig), edits)
+ out, err := diff.Apply(string(orig), edits)
+ if err != nil {
+ t.Errorf("%s: error applying fixes: %v", file.Name(), err)
+ continue
+ }
// the file may contain multiple trailing
// newlines if the user places empty lines
// between files in the archive. normalize
@@ -213,7 +217,11 @@
catchallEdits = append(catchallEdits, edits...)
}
- out := diff.Apply(string(orig), catchallEdits)
+ out, err := diff.Apply(string(orig), catchallEdits)
+ if err != nil {
+ t.Errorf("%s: error applying fixes: %v", file.Name(), err)
+ continue
+ }
want := string(ar.Comment)
formatted, err := format.Source([]byte(out))
diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go
index 3aa1f0b..cac136a 100644
--- a/gopls/internal/hooks/diff.go
+++ b/gopls/internal/hooks/diff.go
@@ -162,8 +162,8 @@
newedits := diff.Strings(before, after)
stat.Newedits = len(newedits)
stat.Newtime = time.Now().Sub(now)
- got := diff.Apply(before, newedits)
- if got != after {
+ got, err := diff.Apply(before, newedits)
+ if err != nil || got != after {
stat.Msg += "FAIL"
disaster(before, after)
stat.save()
diff --git a/gopls/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go
index 17e3e6a..56e3905 100644
--- a/gopls/internal/lsp/cmd/format.go
+++ b/gopls/internal/lsp/cmd/format.go
@@ -76,11 +76,10 @@
if err != nil {
return fmt.Errorf("%v: %v", spn, err)
}
- sedits, err := source.FromProtocolEdits(file.mapper, edits)
+ formatted, sedits, err := source.ApplyProtocolEdits(file.mapper, edits)
if err != nil {
return fmt.Errorf("%v: %v", spn, err)
}
- formatted := diff.Apply(string(file.mapper.Content), sedits)
printIt := true
if c.List {
printIt = false
diff --git a/gopls/internal/lsp/cmd/imports.go b/gopls/internal/lsp/cmd/imports.go
index 7fd5483..4ee6fce 100644
--- a/gopls/internal/lsp/cmd/imports.go
+++ b/gopls/internal/lsp/cmd/imports.go
@@ -81,12 +81,10 @@
}
}
}
- sedits, err := source.FromProtocolEdits(file.mapper, edits)
+ newContent, sedits, err := source.ApplyProtocolEdits(file.mapper, edits)
if err != nil {
return fmt.Errorf("%v: %v", edits, err)
}
- newContent := diff.Apply(string(file.mapper.Content), sedits)
-
filename := file.uri.Filename()
switch {
case t.Write:
diff --git a/gopls/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go
index e0ffa66..58fb07d 100644
--- a/gopls/internal/lsp/cmd/rename.go
+++ b/gopls/internal/lsp/cmd/rename.go
@@ -95,12 +95,10 @@
cmdFile := conn.AddFile(ctx, uri)
filename := cmdFile.uri.Filename()
- // convert LSP-style edits to []diff.TextEdit cuz Spans are handy
- renameEdits, err := source.FromProtocolEdits(cmdFile.mapper, edits[uri])
+ newContent, renameEdits, err := source.ApplyProtocolEdits(cmdFile.mapper, edits[uri])
if err != nil {
return fmt.Errorf("%v: %v", edits, err)
}
- newContent := diff.Apply(string(cmdFile.mapper.Content), renameEdits)
switch {
case r.Write:
diff --git a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go
index b6022a7..9c103de 100644
--- a/gopls/internal/lsp/cmd/suggested_fix.go
+++ b/gopls/internal/lsp/cmd/suggested_fix.go
@@ -142,11 +142,10 @@
}
}
- sedits, err := source.FromProtocolEdits(file.mapper, edits)
+ newContent, sedits, err := source.ApplyProtocolEdits(file.mapper, edits)
if err != nil {
return fmt.Errorf("%v: %v", edits, err)
}
- newContent := diff.Apply(string(file.mapper.Content), sedits)
filename := file.uri.Filename()
switch {
diff --git a/gopls/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go
index bb5fb80..3eb13ea 100644
--- a/gopls/internal/lsp/fake/edit.go
+++ b/gopls/internal/lsp/fake/edit.go
@@ -6,14 +6,16 @@
import (
"fmt"
- "sort"
"strings"
+ "unicode/utf8"
"golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/internal/diff"
)
-// Pos represents a position in a text buffer. Both Line and Column are
-// 0-indexed.
+// Pos represents a position in a text buffer.
+// Both Line and Column are 0-indexed.
+// Column counts runes.
type Pos struct {
Line, Column int
}
@@ -105,78 +107,51 @@
return true
}
-// editContent implements a simplistic, inefficient algorithm for applying text
-// edits to our buffer representation. It returns an error if the edit is
-// invalid for the current content.
-//
-// TODO(rfindley): this function does not handle non-ascii text correctly.
-// TODO(rfindley): replace this with diff.Apply: we should not be
-// maintaining an additional representation of edits.
-func editContent(content []string, edits []Edit) ([]string, error) {
- newEdits := make([]Edit, len(edits))
- copy(newEdits, edits)
- sort.SliceStable(newEdits, func(i, j int) bool {
- ei := newEdits[i]
- ej := newEdits[j]
+// applyEdits applies the edits to a file with the specified lines,
+// and returns a new slice containing the lines of the patched file.
+// It is a wrapper around diff.Apply; see that function for preconditions.
+func applyEdits(lines []string, edits []Edit) ([]string, error) {
+ src := strings.Join(lines, "\n")
- // Sort by edit start position followed by end position. Given an edit
- // 3:1-3:1 followed by an edit 3:1-3:15, we must process the empty edit
- // first.
- if cmp := comparePos(ei.Start, ej.Start); cmp != 0 {
- return cmp < 0
- }
+ // Build a table of byte offset of start of each line.
+ lineOffset := make([]int, len(lines)+1)
+ offset := 0
+ for i, line := range lines {
+ lineOffset[i] = offset
+ offset += len(line) + len("\n")
+ }
+ lineOffset[len(lines)] = offset // EOF
- return comparePos(ei.End, ej.End) < 0
- })
+ var badCol error
+ posToOffset := func(pos Pos) int {
+ offset := lineOffset[pos.Line]
+ // Convert pos.Column (runes) to a UTF-8 byte offset.
+ if pos.Line < len(lines) {
+ for i := 0; i < pos.Column; i++ {
+ r, sz := utf8.DecodeRuneInString(src[offset:])
+ if r == '\n' && badCol == nil {
+ badCol = fmt.Errorf("bad column")
+ }
+ offset += sz
+ }
+ }
+ return offset
+ }
- // Validate edits.
- for _, edit := range newEdits {
- if edit.End.Line < edit.Start.Line || (edit.End.Line == edit.Start.Line && edit.End.Column < edit.Start.Column) {
- return nil, fmt.Errorf("invalid edit: end %v before start %v", edit.End, edit.Start)
- }
- if !inText(edit.Start, content) {
- return nil, fmt.Errorf("start position %v is out of bounds", edit.Start)
- }
- if !inText(edit.End, content) {
- return nil, fmt.Errorf("end position %v is out of bounds", edit.End)
+ // Convert fake.Edits to diff.Edits
+ diffEdits := make([]diff.Edit, len(edits))
+ for i, edit := range edits {
+ diffEdits[i] = diff.Edit{
+ Start: posToOffset(edit.Start),
+ End: posToOffset(edit.End),
+ New: edit.Text,
}
}
- var (
- b strings.Builder
- line, column int
- )
- advance := func(toLine, toColumn int) {
- for ; line < toLine; line++ {
- b.WriteString(string([]rune(content[line])[column:]) + "\n")
- column = 0
- }
- b.WriteString(string([]rune(content[line])[column:toColumn]))
- column = toColumn
+ patched, err := diff.Apply(src, diffEdits)
+ if err != nil {
+ return nil, err
}
- for _, edit := range newEdits {
- advance(edit.Start.Line, edit.Start.Column)
- b.WriteString(edit.Text)
- line = edit.End.Line
- column = edit.End.Column
- }
- advance(len(content)-1, len([]rune(content[len(content)-1])))
- return strings.Split(b.String(), "\n"), nil
-}
-// comparePos returns -1 if left < right, 0 if left == right, and 1 if left > right.
-func comparePos(left, right Pos) int {
- if left.Line < right.Line {
- return -1
- }
- if left.Line > right.Line {
- return 1
- }
- if left.Column < right.Column {
- return -1
- }
- if left.Column > right.Column {
- return 1
- }
- return 0
+ return strings.Split(patched, "\n"), badCol
}
diff --git a/gopls/internal/lsp/fake/edit_test.go b/gopls/internal/lsp/fake/edit_test.go
index 4fa23bd..f87d921 100644
--- a/gopls/internal/lsp/fake/edit_test.go
+++ b/gopls/internal/lsp/fake/edit_test.go
@@ -9,7 +9,7 @@
"testing"
)
-func TestApplyEdit(t *testing.T) {
+func TestApplyEdits(t *testing.T) {
tests := []struct {
label string
content string
@@ -82,7 +82,7 @@
test := test
t.Run(test.label, func(t *testing.T) {
lines := strings.Split(test.content, "\n")
- newLines, err := editContent(lines, test.edits)
+ newLines, err := applyEdits(lines, test.edits)
if (err != nil) != test.wantErr {
t.Errorf("got err %v, want error: %t", err, test.wantErr)
}
diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go
index 17bec28..e65db93 100644
--- a/gopls/internal/lsp/fake/editor.go
+++ b/gopls/internal/lsp/fake/editor.go
@@ -710,9 +710,7 @@
if !ok {
return fmt.Errorf("unknown buffer %q", path)
}
- content := make([]string, len(buf.lines))
- copy(content, buf.lines)
- content, err := editContent(content, edits)
+ content, err := applyEdits(buf.lines, edits)
if err != nil {
return err
}
diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go
index 78032f0..4cd009d 100644
--- a/gopls/internal/lsp/lsp_test.go
+++ b/gopls/internal/lsp/lsp_test.go
@@ -24,7 +24,6 @@
"golang.org/x/tools/gopls/internal/lsp/tests"
"golang.org/x/tools/gopls/internal/lsp/tests/compare"
"golang.org/x/tools/internal/bug"
- "golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/testenv"
@@ -409,11 +408,10 @@
if err != nil {
t.Fatal(err)
}
- sedits, err := source.FromProtocolEdits(m, edits)
+ got, _, err := source.ApplyProtocolEdits(m, edits)
if err != nil {
t.Error(err)
}
- got := diff.Apply(string(m.Content), sedits)
if diff := compare.Text(gofmted, got); diff != "" {
t.Errorf("format failed for %s (-want +got):\n%s", filename, diff)
}
@@ -975,11 +973,10 @@
if err != nil {
t.Fatal(err)
}
- sedits, err := source.FromProtocolEdits(m, edits)
+ got, _, err := source.ApplyProtocolEdits(m, edits)
if err != nil {
t.Error(err)
}
- got := diff.Apply(string(m.Content), sedits)
withinlayHints := string(r.data.Golden(t, "inlayHint", filename, func() ([]byte, error) {
return []byte(got), nil
@@ -1115,29 +1112,16 @@
return nil, err
}
}
- res[uri] = string(m.Content)
- sedits, err := source.FromProtocolEdits(m, docEdits.TextDocumentEdit.Edits)
+ patched, _, err := source.ApplyProtocolEdits(m, docEdits.TextDocumentEdit.Edits)
if err != nil {
return nil, err
}
- res[uri] = applyEdits(res[uri], sedits)
+ res[uri] = patched
}
}
return res, nil
}
-func applyEdits(contents string, edits []diff.Edit) string {
- res := contents
-
- // Apply the edits from the end of the file forward
- // to preserve the offsets
- for i := len(edits) - 1; i >= 0; i-- {
- edit := edits[i]
- res = res[:edit.Start] + edit.New + res[edit.End:]
- }
- return res
-}
-
func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
params := &protocol.DocumentSymbolParams{
TextDocument: protocol.TextDocumentIdentifier{
diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go
index dc7445a..c6e70ef 100644
--- a/gopls/internal/lsp/source/format.go
+++ b/gopls/internal/lsp/source/format.go
@@ -337,6 +337,8 @@
return result, nil
}
+// ToProtocolEdits converts diff.Edits to LSP TextEdits.
+// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray
func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.Edit) ([]protocol.TextEdit, error) {
result := make([]protocol.TextEdit, len(edits))
for i, edit := range edits {
@@ -352,6 +354,8 @@
return result, nil
}
+// ToProtocolEdits converts LSP TextEdits to diff.Edits.
+// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray
func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.Edit, error) {
if edits == nil {
return nil, nil
@@ -370,3 +374,14 @@
}
return result, nil
}
+
+// ApplyProtocolEdits applies the patch (edits) to m.Content and returns the result.
+// It also returns the edits converted to diff-package form.
+func ApplyProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) (string, []diff.Edit, error) {
+ diffEdits, err := FromProtocolEdits(m, edits)
+ if err != nil {
+ return "", nil, err
+ }
+ out, err := diff.Apply(string(m.Content), diffEdits)
+ return out, diffEdits, err
+}
diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go
index 842b1a9..c5af1ca 100644
--- a/gopls/internal/lsp/source/rename.go
+++ b/gopls/internal/lsp/source/rename.go
@@ -446,7 +446,6 @@
return nil, err
}
m := protocol.NewColumnMapper(uri, data)
- diff.SortEdits(edits)
protocolEdits, err := ToProtocolEdits(m, edits)
if err != nil {
return nil, err
diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go
index 0a4e70c..d81bdb7 100644
--- a/gopls/internal/lsp/source/source_test.go
+++ b/gopls/internal/lsp/source/source_test.go
@@ -22,7 +22,6 @@
"golang.org/x/tools/gopls/internal/lsp/tests"
"golang.org/x/tools/gopls/internal/lsp/tests/compare"
"golang.org/x/tools/internal/bug"
- "golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/fuzzy"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/testenv"
@@ -477,19 +476,14 @@
}
return
}
- data, err := fh.Read()
- if err != nil {
- t.Fatal(err)
- }
m, err := r.data.Mapper(spn.URI())
if err != nil {
t.Fatal(err)
}
- diffEdits, err := source.FromProtocolEdits(m, edits)
+ got, _, err := source.ApplyProtocolEdits(m, edits)
if err != nil {
t.Error(err)
}
- got := diff.Apply(string(data), diffEdits)
if gofmted != got {
t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", spn.URI().Filename(), gofmted, got)
}
@@ -508,19 +502,14 @@
if err != nil {
t.Error(err)
}
- data, err := fh.Read()
- if err != nil {
- t.Fatal(err)
- }
m, err := r.data.Mapper(fh.URI())
if err != nil {
t.Fatal(err)
}
- diffEdits, err := source.FromProtocolEdits(m, edits)
+ got, _, err := source.ApplyProtocolEdits(m, edits)
if err != nil {
t.Error(err)
}
- got := diff.Apply(string(data), diffEdits)
want := string(r.data.Golden(t, "goimports", spn.URI().Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
@@ -781,19 +770,14 @@
if err != nil {
t.Fatal(err)
}
- data, err := fh.Read()
- if err != nil {
- t.Fatal(err)
- }
m, err := r.data.Mapper(fh.URI())
if err != nil {
t.Fatal(err)
}
- diffEdits, err := source.FromProtocolEdits(m, edits)
+ contents, _, err := source.ApplyProtocolEdits(m, edits)
if err != nil {
t.Fatal(err)
}
- contents := applyEdits(string(data), diffEdits)
if len(changes) > 1 {
filename := filepath.Base(editURI.Filename())
contents = fmt.Sprintf("%s:\n%s", filename, contents)
@@ -821,18 +805,6 @@
}
}
-func applyEdits(contents string, edits []diff.Edit) string {
- res := contents
-
- // Apply the edits from the end of the file forward
- // to preserve the offsets
- for i := len(edits) - 1; i >= 0; i-- {
- edit := edits[i]
- res = res[:edit.Start] + edit.New + res[edit.End:]
- }
- return res
-}
-
func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) {
// Removed in favor of just using the lsp_test implementation. See ../lsp_test.go
}
diff --git a/internal/diff/diff.go b/internal/diff/diff.go
index e7f8469..a75026d 100644
--- a/internal/diff/diff.go
+++ b/internal/diff/diff.go
@@ -6,6 +6,7 @@
package diff
import (
+ "fmt"
"sort"
"strings"
)
@@ -22,46 +23,71 @@
New string // the replacement
}
-// SortEdits orders edits by their start offset. The sort is stable
-// so that edits with the same start offset will not be reordered.
-func SortEdits(edits []Edit) {
- sort.SliceStable(edits, func(i int, j int) bool {
- return edits[i].Start < edits[j].Start
- })
-}
-
-// Apply applies a sequence of edits to the src buffer and
-// returns the result. It may panic or produce garbage if the edits
-// are overlapping, out of bounds of src, or out of order.
+// Apply applies a sequence of edits to the src buffer and returns the
+// result. Edits are applied in order of start offset; edits with the
+// same start offset are applied in they order they were provided.
//
-// TODO(adonovan): this function must not panic if the edits aren't
-// consistent with src, or with each other---especially when fed
-// information from an untrusted source. It should probably be
-// defensive against bad input and report an error in any of the above
-// situations.
-func Apply(src string, edits []Edit) string {
- SortEdits(edits) // TODO(adonovan): move to caller? What's the contract? Don't mutate arguments.
+// Apply returns an error if any edit is out of bounds,
+// or if any pair of edits is overlapping.
+func Apply(src string, edits []Edit) (string, error) {
+ if !sort.IsSorted(editsSort(edits)) {
+ edits = append([]Edit(nil), edits...)
+ sortEdits(edits)
+ }
- var out strings.Builder
- // TODO(adonovan): opt: preallocate correct final size
- // by scanning the list of edits. (This can be done
- // in the same pass as detecting inconsistent edits.)
- last := 0
+ // Check validity of edits and compute final size.
+ size := len(src)
+ lastEnd := 0
for _, edit := range edits {
- start := edit.Start
- if start > last {
- out.WriteString(src[last:start])
- last = start
+ if !(0 <= edit.Start && edit.Start <= edit.End && edit.End <= len(src)) {
+ return "", fmt.Errorf("diff has out-of-bounds edits")
}
- out.WriteString(edit.New)
- last = edit.End
+ if edit.Start < lastEnd {
+ return "", fmt.Errorf("diff has overlapping edits")
+ }
+ size += len(edit.New) + edit.Start - edit.End
+ lastEnd = edit.End
}
- if last < len(src) {
- out.WriteString(src[last:])
+
+ // Apply edits.
+ out := make([]byte, 0, size)
+ lastEnd = 0
+ for _, edit := range edits {
+ if lastEnd < edit.Start {
+ out = append(out, src[lastEnd:edit.Start]...)
+ }
+ out = append(out, edit.New...)
+ lastEnd = edit.End
}
- return out.String()
+ out = append(out, src[lastEnd:]...)
+
+ if len(out) != size {
+ panic("wrong size")
+ }
+
+ return string(out), nil
}
+// sortEdits orders edits by (start, end) offset.
+// This ordering puts insertions (end=start) before deletions
+// (end>start) at the same point, but uses a stable sort to preserve
+// the order of multiple insertions at the same point.
+// (Apply detects multiple deletions at the same point as an error.)
+func sortEdits(edits editsSort) {
+ sort.Stable(edits)
+}
+
+type editsSort []Edit
+
+func (a editsSort) Len() int { return len(a) }
+func (a editsSort) Less(i, j int) bool {
+ if cmp := a[i].Start - a[j].Start; cmp != 0 {
+ return cmp < 0
+ }
+ return a[i].End < a[j].End
+}
+func (a editsSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
// LineEdits expands and merges a sequence of edits so that each
// resulting edit replaces one or more complete lines.
//
@@ -71,7 +97,7 @@
// We could hide this from the API so that we can enforce
// the precondition... but it seems like a reasonable feature.
func LineEdits(src string, edits []Edit) []Edit {
- SortEdits(edits) // TODO(adonovan): is this necessary? Move burden to caller?
+ sortEdits(edits) // TODO(adonovan): is this necessary? Move burden to caller?
// Do all edits begin and end at the start of a line?
// TODO(adonovan): opt: is this fast path necessary?
diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go
index 8b8b648..d4c3746 100644
--- a/internal/diff/diff_test.go
+++ b/internal/diff/diff_test.go
@@ -18,11 +18,19 @@
func TestApply(t *testing.T) {
for _, tc := range difftest.TestCases {
t.Run(tc.Name, func(t *testing.T) {
- if got := diff.Apply(tc.In, tc.Edits); got != tc.Out {
+ got, err := diff.Apply(tc.In, tc.Edits)
+ if err != nil {
+ t.Fatalf("Apply(Edits) failed: %v", err)
+ }
+ if got != tc.Out {
t.Errorf("Apply(Edits): got %q, want %q", got, tc.Out)
}
if tc.LineEdits != nil {
- if got := diff.Apply(tc.In, tc.LineEdits); got != tc.Out {
+ got, err := diff.Apply(tc.In, tc.LineEdits)
+ if err != nil {
+ t.Fatalf("Apply(LineEdits) failed: %v", err)
+ }
+ if got != tc.Out {
t.Errorf("Apply(LineEdits): got %q, want %q", got, tc.Out)
}
}
@@ -33,7 +41,10 @@
func TestNEdits(t *testing.T) {
for _, tc := range difftest.TestCases {
edits := diff.Strings(tc.In, tc.Out)
- got := diff.Apply(tc.In, edits)
+ got, err := diff.Apply(tc.In, edits)
+ if err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if got != tc.Out {
t.Fatalf("%s: got %q wanted %q", tc.Name, got, tc.Out)
}
@@ -49,7 +60,10 @@
a := randstr("abω", 16)
b := randstr("abωc", 16)
edits := diff.Strings(a, b)
- got := diff.Apply(a, edits)
+ got, err := diff.Apply(a, edits)
+ if err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if got != b {
t.Fatalf("%d: got %q, wanted %q, starting with %q", i, got, b, a)
}
@@ -63,7 +77,10 @@
return // inputs must be text
}
edits := diff.Strings(a, b)
- got := diff.Apply(a, edits)
+ got, err := diff.Apply(a, edits)
+ if err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if got != b {
t.Fatalf("applying diff(%q, %q) gives %q; edits=%v", a, b, got, edits)
}
@@ -90,7 +107,10 @@
}
a, b := strings.SplitAfter(x, "\n"), strings.SplitAfter(y, "\n")
edits := diff.Lines(a, b)
- got := diff.Apply(x, edits)
+ got, err := diff.Apply(x, edits)
+ if err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if got != y {
t.Fatalf("%d: got\n%q, wanted\n%q, starting with %q", i, got, y, a)
}
@@ -134,7 +154,10 @@
b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n"
diffs := diff.Strings(a, b)
- got := diff.Apply(a, diffs)
+ got, err := diff.Apply(a, diffs)
+ if err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if got != b {
i := 0
for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ {
@@ -148,7 +171,10 @@
a := "n\"\n)\n"
b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n"
diffs := diff.Strings(a, b)
- got := diff.Apply(a, diffs)
+ got, err := diff.Apply(a, diffs)
+ if err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if got != b {
i := 0
for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ {
diff --git a/internal/diff/difftest/difftest.go b/internal/diff/difftest/difftest.go
index 998a90f..5c0a741 100644
--- a/internal/diff/difftest/difftest.go
+++ b/internal/diff/difftest/difftest.go
@@ -245,7 +245,10 @@
for _, test := range TestCases {
t.Run(test.Name, func(t *testing.T) {
edits := compute(test.In, test.Out)
- got := diff.Apply(test.In, edits)
+ got, err := diff.Apply(test.In, edits)
+ if err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
unified := diff.Unified(FileA, FileB, test.In, edits)
if got != test.Out {
t.Errorf("Apply: got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.Out)