fix(minErr): stringify non-JSON compatible objects in error messages

Fix the JSON stringification to output a more meaningful string when an
object cannot be normally converted to a JSON string, such as when the
object contains cyclic references that would cause `JSON.stringify()`
to throw an error.

Closes #10085
This commit is contained in:
Peter Bacon Darwin
2014-11-17 07:07:43 +00:00
parent a9352c19ce
commit cf43ccdf9b
7 changed files with 58 additions and 20 deletions

2
angularFiles.js vendored
View File

@@ -5,6 +5,7 @@ var angularFiles = {
'src/minErr.js',
'src/Angular.js',
'src/loader.js',
'src/stringify.js',
'src/AngularPublic.js',
'src/jqLite.js',
'src/apis.js',
@@ -73,6 +74,7 @@ var angularFiles = {
],
'angularLoader': [
'stringify.js',
'src/minErr.js',
'src/loader.js'
],

View File

@@ -19,6 +19,7 @@
"angularModule": false,
"nodeName_": false,
"uid": false,
"toDebugString": false,
"REGEX_STRING_REGEXP" : false,
"lowercase": false,

View File

@@ -37,31 +37,14 @@ function minErr(module, ErrorConstructor) {
prefix = '[' + (module ? module + ':' : '') + code + '] ',
template = arguments[1],
templateArgs = arguments,
stringify = function(obj) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
} else if (typeof obj === 'undefined') {
return 'undefined';
} else if (typeof obj !== 'string') {
return JSON.stringify(obj);
}
return obj;
},
message, i;
message = prefix + template.replace(/\{\d+\}/g, function(match) {
var index = +match.slice(1, -1), arg;
if (index + 2 < templateArgs.length) {
arg = templateArgs[index + 2];
if (typeof arg === 'function') {
return arg.toString().replace(/ ?\{[\s\S]*$/, '');
} else if (typeof arg === 'undefined') {
return 'undefined';
} else if (typeof arg !== 'string') {
return toJson(arg);
}
return arg;
return toDebugString(templateArgs[index + 2]);
}
return match;
});
@@ -70,7 +53,7 @@ function minErr(module, ErrorConstructor) {
(module ? module + '/' : '') + code;
for (i = 2; i < arguments.length; i++) {
message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' +
encodeURIComponent(stringify(arguments[i]));
encodeURIComponent(toDebugString(arguments[i]));
}
return new ErrorConstructor(message);
};

29
src/stringify.js Normal file
View File

@@ -0,0 +1,29 @@
'use strict';
/* 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;
});
}
function toDebugString(obj) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
} else if (typeof obj === 'undefined') {
return 'undefined';
} else if (typeof obj !== 'string') {
return serializeObject(obj);
}
return obj;
}

View File

@@ -18,6 +18,7 @@
"angularModule": false,
"nodeName_": false,
"uid": false,
"toDebugString": false,
"lowercase": false,
"uppercase": false,

View File

@@ -60,6 +60,13 @@ describe('minErr', function() {
toMatch(/^\[test:26\] false: false; zero: 0; null: null; undefined: undefined; emptyStr: /);
});
it('should handle arguments that are objects with cyclic references', function() {
var a = { b: { } };
a.b.a = a;
var myError = testError('26', 'a is {0}', a);
expect(myError.message).toMatch(/a is {"b":{"a":"<<already seen>>"}}/);
});
it('should preserve interpolation markers when fewer arguments than needed are provided', function() {
// this way we can easily see if we are passing fewer args than needed

15
test/stringifySpec.js Normal file
View File

@@ -0,0 +1,15 @@
'use strict';
describe('toDebugString', function() {
it('should convert its argument to a string', function() {
expect(toDebugString('string')).toEqual('string');
expect(toDebugString(123)).toEqual('123');
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 = { };
a.a = a;
expect(toDebugString(a)).toEqual('{"a":"<<already seen>>"}');
expect(toDebugString([a,a])).toEqual('[{"a":"<<already seen>>"},"<<already seen>>"]');
});
});