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

fix(minErr/ngRepeat/$rootScope): serialization of error objects #10085

Closed
wants to merge 3 commits 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
2 changes: 2 additions & 0 deletions angularFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -73,6 +74,7 @@ var angularFiles = {
],

'angularLoader': [
'stringify.js',
'src/minErr.js',
'src/loader.js'
],
Expand Down
1 change: 1 addition & 0 deletions src/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"angularModule": false,
"nodeName_": false,
"uid": false,
"toDebugString": false,

"REGEX_STRING_REGEXP" : false,
"lowercase": false,
Expand Down
23 changes: 3 additions & 20 deletions src/minErr.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand All @@ -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);
};
Expand Down
2 changes: 1 addition & 1 deletion src/ng/directive/ngRepeat.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
});
throw ngRepeatMinErr('dupes',
"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}",
expression, trackById, toJson(value));
expression, trackById, value);
} else {
// new never before seen block
nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined};
Expand Down
12 changes: 6 additions & 6 deletions src/ng/rootScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -773,11 +773,11 @@ function $RootScopeProvider() {
if (ttl < 5) {
logIdx = 4 - ttl;
if (!watchLog[logIdx]) watchLog[logIdx] = [];
logMsg = (isFunction(watch.exp))
? 'fn: ' + (watch.exp.name || watch.exp.toString())
: watch.exp;
logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
watchLog[logIdx].push(logMsg);
watchLog[logIdx].push({
msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp,
newVal: value,
oldVal: last
});
}
} else if (watch === lastDirtyWatch) {
// If the most recently dirty watcher is now clean, short circuit since the remaining watchers
Expand Down Expand Up @@ -810,7 +810,7 @@ function $RootScopeProvider() {
throw $rootScopeMinErr('infdig',
'{0} $digest() iterations reached. Aborting!\n' +
'Watchers fired in the last 5 iterations: {1}',
TTL, toJson(watchLog));
TTL, watchLog);
}

} while (dirty || asyncQueue.length);
Expand Down
29 changes: 29 additions & 0 deletions src/stringify.js
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions test/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"angularModule": false,
"nodeName_": false,
"uid": false,
"toDebugString": false,

"lowercase": false,
"uppercase": false,
Expand Down
7 changes: 7 additions & 0 deletions test/minErrSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions test/ng/rootScopeSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,11 @@ describe('Scope', function() {
$rootScope.$digest();
}).toThrowMinErr('$rootScope', 'infdig', '100 $digest() iterations reached. Aborting!\n' +
'Watchers fired in the last 5 iterations: ' +
'[["a; newVal: 96; oldVal: 95","b; newVal: 97; oldVal: 96"],' +
'["a; newVal: 97; oldVal: 96","b; newVal: 98; oldVal: 97"],' +
'["a; newVal: 98; oldVal: 97","b; newVal: 99; oldVal: 98"],' +
'["a; newVal: 99; oldVal: 98","b; newVal: 100; oldVal: 99"],' +
'["a; newVal: 100; oldVal: 99","b; newVal: 101; oldVal: 100"]]');
'[[{"msg":"a","newVal":96,"oldVal":95},{"msg":"b","newVal":97,"oldVal":96}],' +
'[{"msg":"a","newVal":97,"oldVal":96},{"msg":"b","newVal":98,"oldVal":97}],' +
'[{"msg":"a","newVal":98,"oldVal":97},{"msg":"b","newVal":99,"oldVal":98}],' +
'[{"msg":"a","newVal":99,"oldVal":98},{"msg":"b","newVal":100,"oldVal":99}],' +
'[{"msg":"a","newVal":100,"oldVal":99},{"msg":"b","newVal":101,"oldVal":100}]]');

expect($rootScope.$$phase).toBeNull();
});
Expand Down
15 changes: 15 additions & 0 deletions test/stringifySpec.js
Original file line number Diff line number Diff line change
@@ -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>>"]');
});
});