From c5d1872420a9cc28ed1ccfe65e59570d919f3e1e Mon Sep 17 00:00:00 2001 From: 2ynn Date: Tue, 5 May 2026 18:03:44 -0400 Subject: [PATCH] fix: gen-2943 --- apply.go | 17 ++++++++++++++++- apply_result.go | 8 +++++++- apply_session.go | 11 ++++++++++- apply_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/apply.go b/apply.go index 78ec235..27841ba 100644 --- a/apply.go +++ b/apply.go @@ -149,9 +149,24 @@ func matchFragment(source []fileLine, start int, fragment []fileLine, ignoreWhit } func lineMatches(left, right fileLine, ignoreWhitespace bool) bool { - if left.hasNewline != right.hasNewline || left.eofMarker != right.eofMarker { + if left.hasNewline != right.hasNewline { return false } + + if left.eofMarker != right.eofMarker { + // eofMarker is unreliable on blank lines: a mid-file blank and the + // synthetic source-EOF sentinel both serialize as " \n" in unified + // diff, so the parser can't tell them apart. We accept the position + // match here; applyHunk then writes the correct eofMarker by copying + // it from the matched source line instead of the patch line. + isBlankWithNewLine := func(line fileLine) bool { + return line.text == "" && line.hasNewline + } + + if !isBlankWithNewLine(left) || !isBlankWithNewLine(right) { + return false + } + } if left.text == right.text { return true } diff --git a/apply_result.go b/apply_result.go index 74549a6..a816783 100644 --- a/apply_result.go +++ b/apply_result.go @@ -31,7 +31,13 @@ type applyConflict struct { // applyError reports the aggregate apply outcome. type applyError struct { - DirectMisses int + // DirectMisses counts hunks whose preimage could not be located in the + // pristine file during a direct (non-merge) apply. The output content is + // left unchanged for those regions; no conflict markers are emitted. + DirectMisses int + // MergeConflicts counts hunks whose preimage could not be located during + // a merge-mode apply. The output content contains git-style conflict + // markers (<<<<<<<, =======, >>>>>>>) around each affected region. MergeConflicts int // ConflictingHunks keeps the legacy count available for callers that still // reason about conflict hunks rather than the new miss/conflict split. diff --git a/apply_session.go b/apply_session.go index 4af7dd4..da4a279 100644 --- a/apply_session.go +++ b/apply_session.go @@ -87,7 +87,16 @@ func (s *applySession) applyHunk(hunk patchHunk, match matchedHunk) { for _, hunkLine := range hunk.lines[match.hunkStart:match.hunkEnd] { switch hunkLine.kind { case ' ': - s.image = append(s.image, fileLine{text: hunkLine.text, hasNewline: hunkLine.hasNewline, eofMarker: hunkLine.newEOF}) + // Source eofMarker from the matched source line. The parser's + // hunkLine.newEOF flag is unreliable for blank trailing context + // because markEOFMarkers cannot distinguish a real mid-file + // blank context line from the synthetic source EOF marker + // (see related comment in lineMatches). + eof := hunkLine.newEOF + if s.cursor < len(s.sourceLines) { + eof = s.sourceLines[s.cursor].eofMarker + } + s.image = append(s.image, fileLine{text: hunkLine.text, hasNewline: hunkLine.hasNewline, eofMarker: eof}) s.cursor++ case '-': s.cursor++ diff --git a/apply_test.go b/apply_test.go index 6d3b538..002f359 100644 --- a/apply_test.go +++ b/apply_test.go @@ -1019,3 +1019,41 @@ func TestApplyFile_PreservesExactBytes(t *testing.T) { require.NoError(t, err) assert.True(t, bytes.Equal(files.out, applied)) } + +// blankTrailingContextFixture provides a hunk inserting lines between two +// structural blocks where the trailing context line is blank. markEOFMarkers +// previously flagged any blank context line at the hunk's count boundary as +// an EOF marker, causing it to mismatch a real mid-file blank line in source. +var blankTrailingContextFixture = struct { + original []byte + target []byte +}{ + original: []byte("class Foo {\n void a() {}\n\n void b() {}\n\n void c() {}\n}\n"), + target: []byte("class Foo {\n void a() {}\n\n void b() {}\n\n void inserted() {}\n\n void c() {}\n}\n"), +} + +// TestApplyFile_BlankTrailingContextMidFile is a regression test for the +// fixture above against the direct apply path. +func TestApplyFile_BlankTrailingContextMidFile(t *testing.T) { + t.Parallel() + + f := blankTrailingContextFixture + patch := buildPatchWithContext(t, "Foo.java", f.original, f.target, 3) + applied, err := ApplyFile(f.original, patch) + require.NoError(t, err) + assert.Equal(t, f.target, applied) +} + +// TestApplyFile_BlankTrailingContextWithConflicts mirrors the regression test +// above against the merge mode used by ApplyFileWithConflicts. The bug +// surfaced as a spurious conflict on a freshly generated file because the +// hunk's trailing blank context was misidentified as an EOF marker. +func TestApplyFile_BlankTrailingContextWithConflicts(t *testing.T) { + t.Parallel() + + f := blankTrailingContextFixture + patch := buildPatchWithContext(t, "Foo.java", f.original, f.target, 3) + applied, err := ApplyFileWithConflicts(f.original, patch) + require.NoError(t, err) + assert.Equal(t, f.target, applied) +}