Skip to content

Commit 5c335de

Browse files
authored
Merge pull request #5 from speakeasy-api/fix/eof-marker
fix: mid-file blank context misidentified as EOF
2 parents adba657 + c5d1872 commit 5c335de

4 files changed

Lines changed: 71 additions & 3 deletions

File tree

apply.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,24 @@ func matchFragment(source []fileLine, start int, fragment []fileLine, ignoreWhit
149149
}
150150

151151
func lineMatches(left, right fileLine, ignoreWhitespace bool) bool {
152-
if left.hasNewline != right.hasNewline || left.eofMarker != right.eofMarker {
152+
if left.hasNewline != right.hasNewline {
153153
return false
154154
}
155+
156+
if left.eofMarker != right.eofMarker {
157+
// eofMarker is unreliable on blank lines: a mid-file blank and the
158+
// synthetic source-EOF sentinel both serialize as " \n" in unified
159+
// diff, so the parser can't tell them apart. We accept the position
160+
// match here; applyHunk then writes the correct eofMarker by copying
161+
// it from the matched source line instead of the patch line.
162+
isBlankWithNewLine := func(line fileLine) bool {
163+
return line.text == "" && line.hasNewline
164+
}
165+
166+
if !isBlankWithNewLine(left) || !isBlankWithNewLine(right) {
167+
return false
168+
}
169+
}
155170
if left.text == right.text {
156171
return true
157172
}

apply_result.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ type applyConflict struct {
3131

3232
// applyError reports the aggregate apply outcome.
3333
type applyError struct {
34-
DirectMisses int
34+
// DirectMisses counts hunks whose preimage could not be located in the
35+
// pristine file during a direct (non-merge) apply. The output content is
36+
// left unchanged for those regions; no conflict markers are emitted.
37+
DirectMisses int
38+
// MergeConflicts counts hunks whose preimage could not be located during
39+
// a merge-mode apply. The output content contains git-style conflict
40+
// markers (<<<<<<<, =======, >>>>>>>) around each affected region.
3541
MergeConflicts int
3642
// ConflictingHunks keeps the legacy count available for callers that still
3743
// reason about conflict hunks rather than the new miss/conflict split.

apply_session.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,16 @@ func (s *applySession) applyHunk(hunk patchHunk, match matchedHunk) {
8787
for _, hunkLine := range hunk.lines[match.hunkStart:match.hunkEnd] {
8888
switch hunkLine.kind {
8989
case ' ':
90-
s.image = append(s.image, fileLine{text: hunkLine.text, hasNewline: hunkLine.hasNewline, eofMarker: hunkLine.newEOF})
90+
// Source eofMarker from the matched source line. The parser's
91+
// hunkLine.newEOF flag is unreliable for blank trailing context
92+
// because markEOFMarkers cannot distinguish a real mid-file
93+
// blank context line from the synthetic source EOF marker
94+
// (see related comment in lineMatches).
95+
eof := hunkLine.newEOF
96+
if s.cursor < len(s.sourceLines) {
97+
eof = s.sourceLines[s.cursor].eofMarker
98+
}
99+
s.image = append(s.image, fileLine{text: hunkLine.text, hasNewline: hunkLine.hasNewline, eofMarker: eof})
91100
s.cursor++
92101
case '-':
93102
s.cursor++

apply_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,3 +1019,41 @@ func TestApplyFile_PreservesExactBytes(t *testing.T) {
10191019
require.NoError(t, err)
10201020
assert.True(t, bytes.Equal(files.out, applied))
10211021
}
1022+
1023+
// blankTrailingContextFixture provides a hunk inserting lines between two
1024+
// structural blocks where the trailing context line is blank. markEOFMarkers
1025+
// previously flagged any blank context line at the hunk's count boundary as
1026+
// an EOF marker, causing it to mismatch a real mid-file blank line in source.
1027+
var blankTrailingContextFixture = struct {
1028+
original []byte
1029+
target []byte
1030+
}{
1031+
original: []byte("class Foo {\n void a() {}\n\n void b() {}\n\n void c() {}\n}\n"),
1032+
target: []byte("class Foo {\n void a() {}\n\n void b() {}\n\n void inserted() {}\n\n void c() {}\n}\n"),
1033+
}
1034+
1035+
// TestApplyFile_BlankTrailingContextMidFile is a regression test for the
1036+
// fixture above against the direct apply path.
1037+
func TestApplyFile_BlankTrailingContextMidFile(t *testing.T) {
1038+
t.Parallel()
1039+
1040+
f := blankTrailingContextFixture
1041+
patch := buildPatchWithContext(t, "Foo.java", f.original, f.target, 3)
1042+
applied, err := ApplyFile(f.original, patch)
1043+
require.NoError(t, err)
1044+
assert.Equal(t, f.target, applied)
1045+
}
1046+
1047+
// TestApplyFile_BlankTrailingContextWithConflicts mirrors the regression test
1048+
// above against the merge mode used by ApplyFileWithConflicts. The bug
1049+
// surfaced as a spurious conflict on a freshly generated file because the
1050+
// hunk's trailing blank context was misidentified as an EOF marker.
1051+
func TestApplyFile_BlankTrailingContextWithConflicts(t *testing.T) {
1052+
t.Parallel()
1053+
1054+
f := blankTrailingContextFixture
1055+
patch := buildPatchWithContext(t, "Foo.java", f.original, f.target, 3)
1056+
applied, err := ApplyFileWithConflicts(f.original, patch)
1057+
require.NoError(t, err)
1058+
assert.Equal(t, f.target, applied)
1059+
}

0 commit comments

Comments
 (0)