Skip to content

Commit c376585

Browse files
Fix stringifyReplacer (#601)
* Add tests of broken behaviour * Fix one test * Correct my test and also make it more convincing * Fix canonicalize; get new test passing * Add release notes
1 parent d2fb347 commit c376585

File tree

3 files changed

+75
-3
lines changed

3 files changed

+75
-3
lines changed

release-notes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
- [#581](https://github.com/kpdecker/jsdiff/pull/581) - **fixed some regex operations used for tokenization in `diffWords` taking O(n^2) time** in pathological cases
1111
- [#595](https://github.com/kpdecker/jsdiff/pull/595) - **fixed a crash in patch creation functions when handling a single hunk consisting of a very large number (e.g. >130k) of lines**. (This was caused by spreading indefinitely-large arrays to `.push()` using `.apply` or the spread operator and hitting the JS-implementation-specific limit on the maximum number of arguments to a function, as shown at https://stackoverflow.com/a/56809779/1709587; thus the exact threshold to hit the error will depend on the environment in which you were running JsDiff.)
1212
- [#596](https://github.com/kpdecker/jsdiff/pull/596) - **removed the `merge` function**. Previously JsDiff included an undocumented function called `merge` that was meant to, in some sense, merge patches. It had at least a couple of serious bugs that could lead to it returning unambiguously wrong results, and it was difficult to simply "fix" because it was [unclear precisely what it was meant to do](https://github.com/kpdecker/jsdiff/issues/181#issuecomment-2198319542). For now, the fix is to remove it entirely.
13+
- [#601](https://github.com/kpdecker/jsdiff/pull/601) - **`diffJson`'s `stringifyReplacer` option behaves more like `JSON.stringify`'s `replacer` argument now.** In particular:
14+
* Each key/value pair now gets passed through the replacer once instead of twice
15+
* The `key` passed to the replacer when the top-level object is passed in as `value` is now `""` (previously, was `undefined`), and the `key` passed with an array element is the array index as a string, like `"0"` or `"1"` (previously was whatever the key for the entire array was). Both the new behaviours match that of `JSON.stringify`.
1316

1417
## 7.0.0
1518

src/diff/json.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jsonDiff.tokenize = lineDiff.tokenize;
1010
jsonDiff.castInput = function(value, options) {
1111
const {undefinedReplacement, stringifyReplacer = (k, v) => typeof v === 'undefined' ? undefinedReplacement : v} = options;
1212

13-
return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), stringifyReplacer, ' ');
13+
return typeof value === 'string' ? value : JSON.stringify(canonicalize(value, null, null, stringifyReplacer), null, ' ');
1414
};
1515
jsonDiff.equals = function(left, right, options) {
1616
return Diff.prototype.equals.call(jsonDiff, left.replace(/,([\r\n])/g, '$1'), right.replace(/,([\r\n])/g, '$1'), options);
@@ -25,7 +25,7 @@ export function canonicalize(obj, stack, replacementStack, replacer, key) {
2525
replacementStack = replacementStack || [];
2626

2727
if (replacer) {
28-
obj = replacer(key, obj);
28+
obj = replacer(key === undefined ? '' : key, obj);
2929
}
3030

3131
let i;
@@ -43,7 +43,7 @@ export function canonicalize(obj, stack, replacementStack, replacer, key) {
4343
canonicalizedObj = new Array(obj.length);
4444
replacementStack.push(canonicalizedObj);
4545
for (i = 0; i < obj.length; i += 1) {
46-
canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, key);
46+
canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, String(i));
4747
}
4848
stack.pop();
4949
replacementStack.pop();

test/diff/json.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,75 @@ describe('diff/json', function() {
183183
]);
184184
});
185185

186+
it('should only run each value through stringifyReplacer once', function() {
187+
expect(
188+
diffJson(
189+
{foo: '123ab'},
190+
{foo: '123xy'},
191+
{stringifyReplacer: (k, v) => typeof v === 'string' ? v.slice(0, v.length - 1) : v}
192+
)
193+
).to.deep.equal(
194+
[
195+
{ count: 1, value: '{\n', removed: false, added: false },
196+
{ count: 1, value: ' \"foo\": "123a"\n', added: false, removed: true },
197+
{ count: 1, value: ' \"foo\": "123x"\n', added: true, removed: false },
198+
{ count: 1, value: '}', removed: false, added: false }
199+
]
200+
);
201+
});
202+
203+
it("should pass the same 'key' values to the replacer as JSON.stringify would", function() {
204+
const calls = [],
205+
obj1 = {a: ['q', 'r', 's', {t: []}]},
206+
obj2 = {a: ['x', 'y', 'z', {bla: []}]};
207+
diffJson(
208+
obj1,
209+
obj2,
210+
{stringifyReplacer: (k, v) => {
211+
calls.push([k, v]);
212+
return v;
213+
}}
214+
);
215+
216+
// We run the same objects through JSON.stringify just to make unambiguous when reading this
217+
// test that we're checking for the same key/value pairs that JSON.stringify would pass to
218+
// the replacer.
219+
const jsonStringifyCalls = [];
220+
JSON.stringify(
221+
obj1,
222+
(k, v) => {
223+
jsonStringifyCalls.push([k, v]);
224+
return v;
225+
}
226+
);
227+
JSON.stringify(
228+
obj2,
229+
(k, v) => {
230+
jsonStringifyCalls.push([k, v]);
231+
return v;
232+
}
233+
);
234+
235+
expect(jsonStringifyCalls).to.deep.equal([
236+
['', {a: ['q', 'r', 's', {t: []}]}],
237+
['a', ['q', 'r', 's', {t: []}]],
238+
['0', 'q'],
239+
['1', 'r'],
240+
['2', 's'],
241+
['3', {t: []}],
242+
['t', []],
243+
['', {a: ['x', 'y', 'z', {bla: []}]}],
244+
['a', ['x', 'y', 'z', {bla: []}]],
245+
['0', 'x'],
246+
['1', 'y'],
247+
['2', 'z'],
248+
['3', {bla: []}],
249+
['bla', []]
250+
]);
251+
252+
expect(calls).to.deep.equal(jsonStringifyCalls);
253+
});
254+
186255
it("doesn't throw on Object.create(null)", function() {
187256
let diff;
188257
expect(function() {

0 commit comments

Comments
 (0)