Skip to content

Commit 02158b3

Browse files
committed
fix(toDebugString): adds better handling of cycle objects
As discussed in angular#10085 this commit adds a function for replacing cycle references in object. It is recursive and it knows if the reference has been made in a straight lin e (sibling objects will be left but object referencing same object will be replaced). It also changes the replacement string to '...' because the older one could be mistaken for html tag by browsers.
1 parent 2901c53 commit 02158b3

File tree

3 files changed

+55
-17
lines changed

3 files changed

+55
-17
lines changed

src/stringify.js

+48-12
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,7 @@
33
/* global: toDebugString: true */
44

55
function serializeObject(obj) {
6-
var seen = [];
7-
8-
return JSON.stringify(obj, function(key, val) {
9-
val = toJsonReplacer(key, val);
10-
if (isObject(val)) {
11-
12-
if (seen.indexOf(val) >= 0) return '<<already seen>>';
13-
14-
seen.push(val);
15-
}
16-
return val;
17-
});
6+
return JSON.stringify(decycleObject(obj), toJsonReplacer);
187
}
198

209
function toDebugString(obj) {
@@ -27,3 +16,50 @@ function toDebugString(obj) {
2716
}
2817
return obj;
2918
}
19+
20+
/**
21+
* Loops through object properties and detects circular references.
22+
* Detected circular references are replaced with '...'.
23+
*
24+
* @param {Object} object Object instance
25+
* @param {Array=} seen Private argument, leave it undefined (it is used internally for recursion)
26+
* @returns {Object} Simple representation of an object (plain object or array)
27+
*/
28+
function decycleObject(object, seen) {
29+
// make sure simple types are returned untouched
30+
if (!canContainCircularReference(object)) return object;
31+
32+
// make sure to assign correct type of a safe object
33+
var safeObject = isArray(object) ? [] : {};
34+
35+
// make local copy of the reference array to be sure
36+
// objects are referenced in straight line
37+
seen = seen ? seen.slice() : [];
38+
39+
for (var key in object) {
40+
var property = object[key];
41+
42+
if (canContainCircularReference(property)) {
43+
if (seen.indexOf(property) >= 0) {
44+
safeObject[key] = '...';
45+
} else {
46+
if (seen.indexOf(object) === -1) seen.push(object);
47+
safeObject[key] = decycleObject(property, seen)
48+
}
49+
} else {
50+
safeObject[key] = property;
51+
}
52+
}
53+
54+
return safeObject;
55+
}
56+
57+
/**
58+
* Check if passed object is an enumerable object and has at least one key
59+
*
60+
* @param {Object} object
61+
* @returns {Boolean}
62+
*/
63+
function canContainCircularReference(object) {
64+
return isObject(object) && Object.keys(object).length;
65+
}

test/minErrSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe('minErr', function() {
6565
a.b.a = a;
6666

6767
var myError = testError('26', 'a is {0}', a);
68-
expect(myError.message).toMatch(/a is {"b":{"a":"<<already seen>>"}}/);
68+
expect(myError.message).toMatch(/a is {"b":{"a":"..."}}/);
6969
});
7070

7171
it('should preserve interpolation markers when fewer arguments than needed are provided', function() {

test/stringifySpec.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ describe('toDebugString', function() {
77
expect(toDebugString({a:{b:'c'}})).toEqual('{"a":{"b":"c"}}');
88
expect(toDebugString(function fn() { var a = 10; })).toEqual('function fn()');
99
expect(toDebugString()).toEqual('undefined');
10-
var a = { };
10+
11+
// circular references
12+
var a = {};
1113
a.a = a;
12-
expect(toDebugString(a)).toEqual('{"a":"<<already seen>>"}');
13-
expect(toDebugString([a,a])).toEqual('[{"a":"<<already seen>>"},"<<already seen>>"]');
14+
expect(toDebugString(a)).toEqual('{"a":{"a":"..."}}');
15+
expect(toDebugString([a,a])).toEqual('[{"a":{"a":"..."}},{"a":{"a":"..."}}]');
1416
});
15-
});
17+
});

0 commit comments

Comments
 (0)