Skip to content

Commit 939bb45

Browse files
Fix handling of EOF in createPatch (#535)
* Add (failing) test case from #531 * Fix test I just added (the test itself, not the jsdiff behaviour) * Fix the new test (but make an old one fail as a side effect) * Stop allowing newlineIsToken to be passed to patch-generation functions (... and causing them to generate patches that can't be applied) * Remove impossible case to placate the coverage checker * Add release notes
1 parent 353d117 commit 939bb45

File tree

4 files changed

+74
-54
lines changed

4 files changed

+74
-54
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ Broadly, jsdiff's diff functions all take an old text and a new text and perform
9191
- `context` describes how many lines of context should be included. You can set this to `Number.MAX_SAFE_INTEGER` or `Infinity` to include the entire file content in one hunk.
9292
- `ignoreWhitespace`: Same as in `diffLines`. Defaults to `false`.
9393
- `stripTrailingCr`: Same as in `diffLines`. Defaults to `false`.
94-
- `newlineIsToken`: Same as in `diffLines`. Defaults to `false`.
9594

9695
* `Diff.createPatch(fileName, oldStr, newStr[, oldHeader[, newHeader[, options]]])` - creates a unified diff patch.
9796

release-notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
* The `fuzzFactor` now indicates the maximum [*Levenshtein* distance](https://en.wikipedia.org/wiki/Levenshtein_distance) that there can be between the context shown in a hunk and the actual file content at a location where we try to apply the hunk. (Previously, it represented a maximum [*Hamming* distance](https://en.wikipedia.org/wiki/Hamming_distance), meaning that a single insertion or deletion in the source file could stop a hunk from applying even with a high `fuzzFactor`.)
3333
* A hunk containing a deletion can now only be applied in a context where the line to be deleted actually appears verbatim. (Previously, as long as enough context lines in the hunk matched, `applyPatch` would apply the hunk anyway and delete a completely different line.)
3434
* The context line immediately before and immediately after an insertion must match exactly between the hunk and the file for a hunk to apply. (Previously this was not required.)
35+
- [#535](https://github.com/kpdecker/jsdiff/pull/535) **A bug in patch generation functions is now fixed** that would sometimes previously cause `\ No newline at end of file` to appear in the wrong place in the generated patch, resulting in the patch being invalid.
36+
- [#535](https://github.com/kpdecker/jsdiff/pull/535) **Passing `newlineIsToken: true` to *patch*-generation functions is no longer allowed.** (Passing it to `diffLines` is still supported - it's only functions like `createPatch` where passing `newlineIsToken` is now an error.) Allowing it to be passed never really made sense, since in cases where the option had any effect on the output at all, the effect tended to be causing a garbled patch to be created that couldn't actually be applied to the source file.
3537

3638
## v5.2.0
3739

src/patch/create.js

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHea
1010
if (typeof options.context === 'undefined') {
1111
options.context = 4;
1212
}
13+
if (options.newlineIsToken) {
14+
throw new Error('newlineIsToken may not be used with patch-generation functions, only with diffing functions');
15+
}
1316

1417
if (!options.callback) {
1518
return diffLinesResultToPatch(diffLines(oldStr, newStr, options));
@@ -29,6 +32,9 @@ export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHea
2932
}
3033

3134
function diffLinesResultToPatch(diff) {
35+
// STEP 1: Build up the patch with no "\ No newline at end of file" lines and with the arrays
36+
// of lines containing trailing newline characters. We'll tidy up later...
37+
3238
if(!diff) {
3339
return;
3440
}
@@ -44,7 +50,7 @@ export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHea
4450
oldLine = 1, newLine = 1;
4551
for (let i = 0; i < diff.length; i++) {
4652
const current = diff[i],
47-
lines = current.lines || current.value.replace(/\n$/, '').split('\n');
53+
lines = current.lines || splitLines(current.value);
4854
current.lines = lines;
4955

5056
if (current.added || current.removed) {
@@ -91,20 +97,6 @@ export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHea
9197
newLines: (newLine - newRangeStart + contextSize),
9298
lines: curRange
9399
};
94-
if (i >= diff.length - 2 && lines.length <= options.context) {
95-
// EOF is inside this hunk
96-
let oldEOFNewline = ((/\n$/).test(oldStr));
97-
let newEOFNewline = ((/\n$/).test(newStr));
98-
let noNlBeforeAdds = lines.length == 0 && curRange.length > hunk.oldLines;
99-
if (!oldEOFNewline && noNlBeforeAdds && oldStr.length > 0) {
100-
// special case: old has no eol and no trailing context; no-nl can end up before adds
101-
// however, if the old file is empty, do not output the no-nl line
102-
curRange.splice(hunk.oldLines, 0, '\\ No newline at end of file');
103-
}
104-
if ((!oldEOFNewline && !noNlBeforeAdds) || !newEOFNewline) {
105-
curRange.push('\\ No newline at end of file');
106-
}
107-
}
108100
hunks.push(hunk);
109101

110102
oldRangeStart = 0;
@@ -117,6 +109,19 @@ export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHea
117109
}
118110
}
119111

112+
// Step 2: eliminate the trailing `\n` from each line of each hunk, and, where needed, add
113+
// "\ No newline at end of file".
114+
for (const hunk of hunks) {
115+
for (let i = 0; i < hunk.lines.length; i++) {
116+
if (hunk.lines[i].endsWith('\n')) {
117+
hunk.lines[i] = hunk.lines[i].slice(0, -1);
118+
} else {
119+
hunk.lines.splice(i + 1, 0, '\\ No newline at end of file');
120+
i++; // Skip the line we just added, then continue iterating
121+
}
122+
}
123+
}
124+
120125
return {
121126
oldFileName: oldFileName, newFileName: newFileName,
122127
oldHeader: oldHeader, newHeader: newHeader,
@@ -197,3 +202,17 @@ export function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, ol
197202
export function createPatch(fileName, oldStr, newStr, oldHeader, newHeader, options) {
198203
return createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldHeader, newHeader, options);
199204
}
205+
206+
/**
207+
* Split `text` into an array of lines, including the trailing newline character (where present)
208+
*/
209+
function splitLines(text) {
210+
const hasTrailingNl = text.endsWith('\n');
211+
const result = text.split('\n').map(line => line + '\n');
212+
if (hasTrailingNl) {
213+
result.pop();
214+
} else {
215+
result.push(result.pop().slice(0, -1));
216+
}
217+
return result;
218+
}

test/patch/create.js

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,39 @@ describe('patch/create', function() {
106106
+ '\\ No newline at end of file\n');
107107
});
108108

109+
it('should get the "No newline" position right in the case from https://github.com/kpdecker/jsdiff/issues/531', function() {
110+
const oldContent = '1st line.\n2nd line.\n3rd line.';
111+
const newContent = 'Z11 thing.\nA New thing.\n2nd line.\nNEW LINE.\n3rd line.\n\nSOMETHING ELSE.';
112+
113+
const diff = createPatch(
114+
'a.txt',
115+
oldContent,
116+
newContent,
117+
undefined,
118+
undefined,
119+
{ context: 3 },
120+
);
121+
122+
expect(diff).to.equal(
123+
'Index: a.txt\n'
124+
+ '===================================================================\n'
125+
+ '--- a.txt\n'
126+
+ '+++ a.txt\n'
127+
+ '@@ -1,3 +1,7 @@\n'
128+
+ '-1st line.\n'
129+
+ '+Z11 thing.\n'
130+
+ '+A New thing.\n'
131+
+ ' 2nd line.\n'
132+
+ '-3rd line.\n'
133+
+ '\\ No newline at end of file\n'
134+
+ '+NEW LINE.\n'
135+
+ '+3rd line.\n'
136+
+ '+\n'
137+
+ '+SOMETHING ELSE.\n'
138+
+ '\\ No newline at end of file\n'
139+
);
140+
});
141+
109142
it('should output "no newline" at end of file message on context missing nl', function() {
110143
expect(createPatch('test', 'line11\nline2\nline3\nline4', 'line1\nline2\nline3\nline4', 'header1', 'header2')).to.equal(
111144
'Index: test\n'
@@ -669,47 +702,14 @@ describe('patch/create', function() {
669702
});
670703

671704
describe('newlineIsToken', function() {
672-
it('newlineIsToken: false', function() {
673-
const expectedResult =
674-
'Index: testFileName\n'
675-
+ '===================================================================\n'
676-
+ '--- testFileName\n'
677-
+ '+++ testFileName\n'
678-
+ '@@ -1,2 +1,2 @@\n'
679-
680-
// Diff is shown as entire row, even though text is unchanged
681-
+ '-line\n'
682-
+ '+line\r\n'
683-
684-
+ ' line\n'
685-
+ '\\ No newline at end of file\n';
686-
687-
const diffResult = createPatch('testFileName', 'line\nline', 'line\r\nline', undefined, undefined, {newlineIsToken: false});
688-
expect(diffResult).to.equal(expectedResult);
689-
});
690-
691-
it('newlineIsToken: true', function() {
692-
const expectedResult =
693-
'Index: testFileName\n'
694-
+ '===================================================================\n'
695-
+ '--- testFileName\n'
696-
+ '+++ testFileName\n'
697-
+ '@@ -1,3 +1,3 @@\n'
698-
+ ' line\n'
699-
700-
// Newline change is shown as a single diff
701-
+ '-\n'
702-
+ '+\r\n'
703-
704-
+ ' line\n'
705-
+ '\\ No newline at end of file\n';
706-
707-
const diffResult = createPatch('testFileName', 'line\nline', 'line\r\nline', undefined, undefined, {newlineIsToken: true});
708-
expect(diffResult).to.equal(expectedResult);
705+
// See https://github.com/kpdecker/jsdiff/pull/345#issuecomment-2255886105
706+
it("isn't allowed any more, since the patches produced were nonsense", function() {
707+
expect(() => {
708+
createPatch('testFileName', 'line\nline', 'line\r\nline', undefined, undefined, {newlineIsToken: true});
709+
}).to['throw']('newlineIsToken may not be used with patch-generation functions, only with diffing functions');
709710
});
710711
});
711712

712-
713713
it('takes an optional callback option', function(done) {
714714
createPatch(
715715
'test',

0 commit comments

Comments
 (0)