Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

fix(toDebugString): adds better handling of cycle objects #10099

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 53 additions & 12 deletions src/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,7 @@
/* global: toDebugString: true */

function serializeObject(obj) {
var seen = [];

return JSON.stringify(obj, function(key, val) {
val = toJsonReplacer(key, val);
if (isObject(val)) {

if (seen.indexOf(val) >= 0) return '<<already seen>>';

seen.push(val);
}
return val;
});
return JSON.stringify(decycleObject(obj), toJsonReplacer);
}

function toDebugString(obj) {
Expand All @@ -27,3 +16,55 @@ function toDebugString(obj) {
}
return obj;
}

/**
* Loops through object properties and detects circular references.
* Detected circular references are replaced with '...'.
*
* @param {Object} object Object instance
* @param {Array=} seen Private argument, leave it undefined (it is used internally for recursion)
* @returns {Object} Simple representation of an object (plain object or array)
*/
function decycleObject(object, seen) {
// make sure simple types are returned untouched
if (!canContainCircularReference(object)) return object;

// make sure to assign correct type of a safe object
var safeObject = isArray(object) ? [] : {};

// make local copy of the reference array to be sure
// objects are referenced in straight line
seen = seen ? seen.slice() : [];

for (var key in object) {
var property = object[key];

if (canContainCircularReference(property)) {
if (seen.indexOf(property) >= 0) {
safeObject[key] = '...';
} else {
if (seen.indexOf(object) === -1) seen.push(object);
safeObject[key] = decycleObject(property, seen);
}
} else {
safeObject[key] = property;
}
}

return safeObject;
}

/**
* Check if passed object is an enumerable object and has at least one key
*
* @param {Object} object
* @returns {Boolean}
*/
function canContainCircularReference(object) {
if (isObject(object)) {
for (var i in object) {
return true;
}
}
return false;
}
2 changes: 1 addition & 1 deletion test/minErrSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('minErr', function() {
a.b.a = a;

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

it('should preserve interpolation markers when fewer arguments than needed are provided', function() {
Expand Down
8 changes: 5 additions & 3 deletions test/stringifySpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ describe('toDebugString', function() {
expect(toDebugString({a:{b:'c'}})).toEqual('{"a":{"b":"c"}}');
expect(toDebugString(function fn() { var a = 10; })).toEqual('function fn()');
expect(toDebugString()).toEqual('undefined');
var a = { };

// circular references
var a = {};
a.a = a;
expect(toDebugString(a)).toEqual('{"a":"<<already seen>>"}');
expect(toDebugString([a,a])).toEqual('[{"a":"<<already seen>>"},"<<already seen>>"]');
expect(toDebugString(a)).toEqual('{"a":{"a":"..."}}');
expect(toDebugString([a,a])).toEqual('[{"a":{"a":"..."}},{"a":{"a":"..."}}]');
});
});