From 86b2fc3d781fb2eff37790a6b0376b7311a07db0 Mon Sep 17 00:00:00 2001 From: kwypchlo Date: Mon, 17 Nov 2014 18:46:35 +0100 Subject: [PATCH] fix(toDebugString): adds better handling of cycle objects As discussed in #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. --- src/stringify.js | 65 +++++++++++++++++++++++++++++++++++-------- test/minErrSpec.js | 2 +- test/stringifySpec.js | 8 ++++-- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/stringify.js b/src/stringify.js index 2c1a343c3701..8506f37bb624 100644 --- a/src/stringify.js +++ b/src/stringify.js @@ -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 '<>'; - - seen.push(val); - } - return val; - }); + return JSON.stringify(decycleObject(obj), toJsonReplacer); } function toDebugString(obj) { @@ -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; +} diff --git a/test/minErrSpec.js b/test/minErrSpec.js index 848188c98ab0..bf1af6d60eea 100644 --- a/test/minErrSpec.js +++ b/test/minErrSpec.js @@ -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":"<>"}}/); + expect(myError.message).toMatch(/a is {"b":{"a":"..."}}/); }); it('should preserve interpolation markers when fewer arguments than needed are provided', function() { diff --git a/test/stringifySpec.js b/test/stringifySpec.js index e849b3e86cb8..632cc128af20 100644 --- a/test/stringifySpec.js +++ b/test/stringifySpec.js @@ -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":"<>"}'); - expect(toDebugString([a,a])).toEqual('[{"a":"<>"},"<>"]'); + expect(toDebugString(a)).toEqual('{"a":{"a":"..."}}'); + expect(toDebugString([a,a])).toEqual('[{"a":{"a":"..."}},{"a":{"a":"..."}}]'); }); });