Repository: kythe/kythe
Pull request #5442 (2022-11-10 23:11:53)

fix: correctly patch zero-width spans


Patch:
diff --git a/kythe/go/util/span/span.go b/kythe/go/util/span/span.go
index e7cd0ae3a..4b20886c2 100644
--- a/kythe/go/util/span/span.go
+++ b/kythe/go/util/span/span.go
@@ -215,8 +215,7 @@ func (t *offsetTracker) Update(d diff) {
 // in newText or is invalid, the returned bool will be false.  As a convenience,
 // if p==nil, the original span will be returned.
 func (p *Patcher) PatchSpan(s *cpb.Span) (span *cpb.Span, exists bool) {
-	spanStart := s.GetStart().GetByteOffset()
-	spanEnd := s.GetEnd().GetByteOffset()
+	spanStart, spanEnd := ByteOffsets(s)
 	if spanStart > spanEnd {
 		return nil, false
 	} else if p == nil || s == nil {
@@ -272,6 +271,13 @@ func (p *Patcher) Patch(spanStart, spanEnd int32) (newStart, newEnd int32, exist
 		return spanStart, spanEnd, true
 	}
 
+	if spanStart == spanEnd {
+		// Give zero-width span a positive length for the below algorithm; then fix
+		// the length on return.
+		spanEnd++
+		defer func() { newEnd = newStart }()
+	}
+
 	var old, new int32
 	for _, d := range p.spans {
 		l := d.Length


Test patch:
diff --git a/kythe/go/util/span/span_test.go b/kythe/go/util/span/span_test.go
index 33dcb26e7..8bdcda265 100644
--- a/kythe/go/util/span/span_test.go
+++ b/kythe/go/util/span/span_test.go
@@ -127,35 +127,50 @@ func TestPatcher(t *testing.T) {
 	}{{
 		oldText:  "this is some text",
 		newText:  "this is some changed text",
-		oldSpans: []*span{{0, 5}, {13, 17}, {8, 17}},
-		newSpans: []*span{{0, 5}, {21, 25}, nil},
+		oldSpans: []*span{{0, 5}, {13, 17}, {13, 13}, {8, 17}},
+		newSpans: []*span{{0, 5}, {21, 25}, {21, 21}, nil},
 	}, {
 		oldText:  "line one\nline two\nline three\nline four\n",
 		newText:  "line one\nline three\nline two\nline four\n",
 		oldSpans: []*span{{0, 10}, {10, 14}, {16, 19}, {20, 24}, {25, 30}, {29, 38}},
 		newSpans: []*span{{0, 10}, {10, 14}, nil, {22, 26}, nil, {29, 38}},
+	}, {
+		oldText: "line three\n",
+		newText: "line one\ntwo\nthree\n",
+		oldSpans: []*span{
+			{0, 4},
+			{5, 5},
+			{5, 10},
+		},
+		newSpans: []*span{
+			{0, 4},
+			{13, 13},
+			{13, 18},
+		},
 	}}
 
-	for _, test := range tests {
-		p, err := NewPatcher([]byte(test.oldText), []byte(test.newText))
-		if err != nil {
-			t.Fatal(err)
-		}
-		if len(test.oldSpans) != len(test.newSpans) {
-			t.Fatalf("Invalid test: {%v}", test)
-		}
-
-		for i, s := range test.oldSpans {
-			start, end, exists := p.Patch(s.start, s.end)
+	for i, test := range tests {
+		t.Run(strconv.Itoa(i), func(t *testing.T) {
+			p, err := NewPatcher([]byte(test.oldText), []byte(test.newText))
+			if err != nil {
+				t.Fatal(err)
+			}
+			if len(test.oldSpans) != len(test.newSpans) {
+				t.Fatalf("Invalid test: {%v}", test)
+			}
 
-			if ns := test.newSpans[i]; ns == nil && exists {
-				t.Errorf("Expected span not to exist in new text; received (%d, %d]", start, end)
-			} else if ns != nil && !exists {
-				t.Errorf("Expected span %v to exist in new text as %v; did not exist", s, ns)
-			} else if ns != nil && exists && (start != ns.start || end != ns.end) {
-				t.Errorf("Expected %v; received (%d, %d]", ns, start, end)
+			for i, s := range test.oldSpans {
+				start, end, exists := p.Patch(s.Start, s.End)
+
+				if ns := test.newSpans[i]; ns == nil && exists {
+					t.Errorf("Expected span not to exist in new text; received (%d, %d]", start, end)
+				} else if ns != nil && !exists {
+					t.Errorf("Expected span %v to exist in new text as %v; did not exist", s, ns)
+				} else if ns != nil && exists && (start != ns.Start || end != ns.End) {
+					t.Errorf("Expected %v; received (%d, %d]", ns, start, end)
+				}
 			}
-		}
+		})
 	}
 }
 
@@ -172,6 +187,18 @@ func TestPatchSpan(t *testing.T) {
 		oldSpans []*cpb.Span
 		newSpans []*cpb.Span
 	}{{
+		oldText: "this is some text\nsecond line\n",
+		newText: "this is some text\nsecond line\n",
+
+		oldSpans: []*cpb.Span{
+			sp(p(0, 1, 0), p(4, 1, 4)),
+			sp(p(13, 1, 13), p(17, 1, 17)),
+		},
+		newSpans: []*cpb.Span{
+			sp(p(0, 1, 0), p(4, 1, 4)),
+			sp(p(13, 1, 13), p(17, 1, 17)),
+		},
+	}, {
 		oldText: "this is some text",
 		newText: "this is some changed text",
 
@@ -218,11 +245,13 @@ func TestPatchSpan(t *testing.T) {
 		newText: "line one\nline two\nthee\n",
 
 		oldSpans: []*cpb.Span{
+			sp(p(0, 1, 0), p(0, 1, 0)),
 			sp(p(0, 1, 0), p(4, 1, 4)),
 			sp(p(5, 2, 0), p(8, 2, 3)),
 			sp(p(9, 3, 0), p(14, 3, 5)),
 		},
 		newSpans: []*cpb.Span{
+			sp(p(0, 1, 0), p(0, 1, 0)),
 			sp(p(0, 1, 0), p(4, 1, 4)),
 			sp(p(14, 2, 5), p(17, 2, 8)),
 			nil,
@@ -233,10 +262,12 @@ func TestPatchSpan(t *testing.T) {
 
 		oldSpans: []*cpb.Span{
 			sp(p(0, 1, 0), p(4, 1, 4)),
+			sp(p(5, 1, 5), p(5, 1, 5)),
 			sp(p(5, 1, 5), p(10, 1, 10)),
 		},
 		newSpans: []*cpb.Span{
 			sp(p(0, 1, 0), p(4, 1, 4)),
+			sp(p(13, 3, 0), p(13, 3, 0)),
 			sp(p(13, 3, 0), p(18, 3, 5)),
 		},
 	}, {
@@ -338,6 +369,6 @@ func TestMarshal(t *testing.T) {
 	}
 }
 
-type span struct{ start, end int32 }
+type span struct{ Start, End int32 }
 
-func (s span) String() string { return fmt.Sprintf("(%d, %d]", s.start, s.end) }
+func (s span) String() string { return fmt.Sprintf("(%d, %d]", s.Start, s.End) }