Skip to content

Commit d12c46d

Browse files
committed
canonicalize: Keep track of the replacement objects so the correct object can be substituted when a circular reference is detected.
1 parent d786a63 commit d12c46d

File tree

2 files changed

+52
-6
lines changed

2 files changed

+52
-6
lines changed

Diff for: diff.js

+15-6
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,17 @@ var JsDiff = (function() {
197197

198198
var objectPrototypeToString = Object.prototype.toString;
199199

200-
function canonicalize(obj, stack) {
200+
// This function handles the presence of circular references by bailing out when encountering an
201+
// object that is already on the "stack" of items being processed.
202+
function canonicalize(obj, stack, replacementStack) {
201203
stack = stack || [];
204+
replacementStack = replacementStack || [];
202205

203206
var i;
204207

205208
for (var i = 0 ; i < stack.length ; i += 1) {
206209
if (stack[i] === obj) {
207-
return obj;
210+
return replacementStack[i];
208211
}
209212
}
210213

@@ -213,28 +216,32 @@ var JsDiff = (function() {
213216
if ('[object Array]' === objectPrototypeToString.call(obj)) {
214217
stack.push(obj);
215218
canonicalizedObj = new Array(obj.length);
219+
replacementStack.push(canonicalizedObj);
216220
for (i = 0 ; i < obj.length ; i += 1) {
217-
canonicalizedObj[i] = canonicalize(obj[i], stack);
221+
canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack);
218222
}
219223
stack.pop();
224+
replacementStack.pop();
220225
} else if (typeof obj === 'object' && obj !== null) {
221226
stack.push(obj);
222227
canonicalizedObj = {};
228+
replacementStack.push(canonicalizedObj);
223229
var sortedKeys = [];
224230
for (var key in obj) {
225231
sortedKeys.push(key);
226232
}
227233
sortedKeys.sort();
228234
for (i = 0 ; i < sortedKeys.length ; i += 1) {
229235
var key = sortedKeys[i];
230-
canonicalizedObj[key] = canonicalize(obj[key], stack);
236+
canonicalizedObj[key] = canonicalize(obj[key], stack, replacementStack);
231237
}
232238
stack.pop();
239+
replacementStack.pop();
233240
} else {
234241
canonicalizedObj = obj;
235242
}
236243
return canonicalizedObj;
237-
}
244+
};
238245

239246
return {
240247
Diff: Diff,
@@ -420,7 +427,9 @@ var JsDiff = (function() {
420427
ret.push([(change.added ? 1 : change.removed ? -1 : 0), change.value]);
421428
}
422429
return ret;
423-
}
430+
},
431+
432+
canonicalize: canonicalize
424433
};
425434
})();
426435

Diff for: test/canonicalize.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const VERBOSE = false;
2+
3+
var diff = require('../diff');
4+
5+
function getKeys(obj) {
6+
var keys = [];
7+
for (var key in obj) {
8+
if (obj.hasOwnProperty(key)) {
9+
keys.push(key);
10+
}
11+
}
12+
return keys;
13+
}
14+
15+
describe('#canonicalize', function() {
16+
it('should put the keys in canonical order', function() {
17+
getKeys(diff.canonicalize({b: 456, a: 123})).should.eql(['a', 'b']);
18+
});
19+
20+
it('should dive into nested objects', function() {
21+
var canonicalObj = diff.canonicalize({b: 456, a: {d: 123, c: 456}});
22+
getKeys(canonicalObj.a).should.eql(['c', 'd']);
23+
});
24+
25+
it('should dive into nested arrays', function() {
26+
var canonicalObj = diff.canonicalize({b: 456, a: [789, {d: 123, c: 456}]});
27+
getKeys(canonicalObj.a[1]).should.eql(['c', 'd']);
28+
});
29+
30+
it('should handle circular references correctly', function() {
31+
var obj = {b: 456};
32+
obj.a = obj;
33+
var canonicalObj = diff.canonicalize(obj);
34+
getKeys(canonicalObj).should.eql(['a', 'b']);
35+
getKeys(canonicalObj.a).should.eql(['a', 'b']);
36+
});
37+
});

0 commit comments

Comments
 (0)