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

Commit a0641ea

Browse files
m-amrgkalpak
authored andcommitted
feat(errorHandlingConfig): make the depth for object stringification in errors configurable
Closes #15402 Closes #15433
1 parent 538f460 commit a0641ea

9 files changed

+191
-25
lines changed

src/.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"splice": false,
1616
"push": false,
1717
"toString": false,
18+
"minErrConfig": false,
19+
"errorHandlingConfig": false,
20+
"isValidObjectMaxDepth": false,
1821
"ngMinErr": false,
1922
"_angular": false,
2023
"angularModule": false,

src/Angular.js

+62-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
splice,
1111
push,
1212
toString,
13+
minErrConfig,
14+
errorHandlingConfig,
15+
isValidObjectMaxDepth,
1316
ngMinErr,
1417
angularModule,
1518
uid,
@@ -125,6 +128,50 @@ var VALIDITY_STATE_PROPERTY = 'validity';
125128

126129
var hasOwnProperty = Object.prototype.hasOwnProperty;
127130

131+
var minErrConfig = {
132+
objectMaxDepth: 5
133+
};
134+
135+
/**
136+
* @ngdoc function
137+
* @name angular.errorHandlingConfig
138+
* @module ng
139+
* @kind function
140+
*
141+
* @description
142+
* Configure several aspects of error handling in AngularJS if used as a setter or return the
143+
* current configuration if used as a getter. The following options are supported:
144+
*
145+
* - **objectMaxDepth**: The maximum depth to which objects are traversed when stringified for error messages.
146+
*
147+
* Omitted or undefined options will leave the corresponding configuration values unchanged.
148+
*
149+
* @param {Object=} config - The configuration object. May only contain the options that need to be
150+
* updated. Supported keys:
151+
*
152+
* * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a
153+
* non-positive or non-numeric value, removes the max depth limit.
154+
* Default: 5
155+
*/
156+
function errorHandlingConfig(config) {
157+
if (isObject(config)) {
158+
if (isDefined(config.objectMaxDepth)) {
159+
minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN;
160+
}
161+
} else {
162+
return minErrConfig;
163+
}
164+
}
165+
166+
/**
167+
* @private
168+
* @param {Number} maxDepth
169+
* @return {boolean}
170+
*/
171+
function isValidObjectMaxDepth(maxDepth) {
172+
return isNumber(maxDepth) && maxDepth > 0;
173+
}
174+
128175
/**
129176
* @ngdoc function
130177
* @name angular.lowercase
@@ -847,9 +894,10 @@ function arrayRemove(array, value) {
847894
</file>
848895
</example>
849896
*/
850-
function copy(source, destination) {
897+
function copy(source, destination, maxDepth) {
851898
var stackSource = [];
852899
var stackDest = [];
900+
maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN;
853901

854902
if (destination) {
855903
if (isTypedArray(destination) || isArrayBuffer(destination)) {
@@ -872,43 +920,47 @@ function copy(source, destination) {
872920

873921
stackSource.push(source);
874922
stackDest.push(destination);
875-
return copyRecurse(source, destination);
923+
return copyRecurse(source, destination, maxDepth);
876924
}
877925

878-
return copyElement(source);
926+
return copyElement(source, maxDepth);
879927

880-
function copyRecurse(source, destination) {
928+
function copyRecurse(source, destination, maxDepth) {
929+
maxDepth--;
930+
if (maxDepth < 0) {
931+
return '...';
932+
}
881933
var h = destination.$$hashKey;
882934
var key;
883935
if (isArray(source)) {
884936
for (var i = 0, ii = source.length; i < ii; i++) {
885-
destination.push(copyElement(source[i]));
937+
destination.push(copyElement(source[i], maxDepth));
886938
}
887939
} else if (isBlankObject(source)) {
888940
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
889941
for (key in source) {
890-
destination[key] = copyElement(source[key]);
942+
destination[key] = copyElement(source[key], maxDepth);
891943
}
892944
} else if (source && typeof source.hasOwnProperty === 'function') {
893945
// Slow path, which must rely on hasOwnProperty
894946
for (key in source) {
895947
if (source.hasOwnProperty(key)) {
896-
destination[key] = copyElement(source[key]);
948+
destination[key] = copyElement(source[key], maxDepth);
897949
}
898950
}
899951
} else {
900952
// Slowest path --- hasOwnProperty can't be called as a method
901953
for (key in source) {
902954
if (hasOwnProperty.call(source, key)) {
903-
destination[key] = copyElement(source[key]);
955+
destination[key] = copyElement(source[key], maxDepth);
904956
}
905957
}
906958
}
907959
setHashKey(destination, h);
908960
return destination;
909961
}
910962

911-
function copyElement(source) {
963+
function copyElement(source, maxDepth) {
912964
// Simple values
913965
if (!isObject(source)) {
914966
return source;
@@ -937,7 +989,7 @@ function copy(source, destination) {
937989
stackDest.push(destination);
938990

939991
return needsRecurse
940-
? copyRecurse(source, destination)
992+
? copyRecurse(source, destination, maxDepth)
941993
: destination;
942994
}
943995

src/AngularPublic.js

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ var version = {
126126

127127
function publishExternalAPI(angular) {
128128
extend(angular, {
129+
'errorHandlingConfig': errorHandlingConfig,
129130
'bootstrap': bootstrap,
130131
'copy': copy,
131132
'extend': extend,

src/minErr.js

+10-12
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,19 @@
3333
function minErr(module, ErrorConstructor) {
3434
ErrorConstructor = ErrorConstructor || Error;
3535
return function() {
36-
var SKIP_INDEXES = 2;
37-
38-
var templateArgs = arguments,
39-
code = templateArgs[0],
36+
var code = arguments[0],
37+
template = arguments[1],
4038
message = '[' + (module ? module + ':' : '') + code + '] ',
41-
template = templateArgs[1],
39+
templateArgs = sliceArgs(arguments, 2).map(function(arg) {
40+
return toDebugString(arg, minErrConfig.objectMaxDepth);
41+
}),
4242
paramPrefix, i;
4343

4444
message += template.replace(/\{\d+\}/g, function(match) {
45-
var index = +match.slice(1, -1),
46-
shiftedIndex = index + SKIP_INDEXES;
45+
var index = +match.slice(1, -1);
4746

48-
if (shiftedIndex < templateArgs.length) {
49-
return toDebugString(templateArgs[shiftedIndex]);
47+
if (index < templateArgs.length) {
48+
return templateArgs[index];
5049
}
5150

5251
return match;
@@ -55,9 +54,8 @@ function minErr(module, ErrorConstructor) {
5554
message += '\nhttp://errors.angularjs.org/"NG_VERSION_FULL"/' +
5655
(module ? module + '/' : '') + code;
5756

58-
for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
59-
message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
60-
encodeURIComponent(toDebugString(templateArgs[i]));
57+
for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
58+
message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
6159
}
6260

6361
return new ErrorConstructor(message);

src/stringify.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22

33
/* global toDebugString: true */
44

5-
function serializeObject(obj) {
5+
function serializeObject(obj, maxDepth) {
66
var seen = [];
77

8+
// There is no direct way to stringify object until reaching a specific depth
9+
// and a very deep object can cause a performance issue, so we copy the object
10+
// based on this specific depth and then stringify it.
11+
if (isValidObjectMaxDepth(maxDepth)) {
12+
obj = copy(obj, null, maxDepth);
13+
}
814
return JSON.stringify(obj, function(key, val) {
915
val = toJsonReplacer(key, val);
1016
if (isObject(val)) {
@@ -17,13 +23,13 @@ function serializeObject(obj) {
1723
});
1824
}
1925

20-
function toDebugString(obj) {
26+
function toDebugString(obj, maxDepth) {
2127
if (typeof obj === 'function') {
2228
return obj.toString().replace(/ \{[\s\S]*$/, '');
2329
} else if (isUndefined(obj)) {
2430
return 'undefined';
2531
} else if (typeof obj !== 'string') {
26-
return serializeObject(obj);
32+
return serializeObject(obj, maxDepth);
2733
}
2834
return obj;
2935
}

test/.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
/* angular.js */
2727
"angular": false,
28+
"minErrConfig": false,
29+
"errorHandlingConfig": false,
2830
"msie": false,
2931
"jqLite": false,
3032
"jQuery": false,
@@ -37,6 +39,7 @@
3739
"nodeName_": false,
3840
"uid": false,
3941
"toDebugString": false,
42+
"serializeObject": false,
4043

4144
"lowercase": false,
4245
"uppercase": false,

test/AngularSpec.js

+25
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,31 @@ describe('angular', function() {
602602
expect(copy(new Number(NaN)).valueOf()).toBeNaN();
603603
/* eslint-enable */
604604
});
605+
606+
it('should copy source until reaching a given max depth', function() {
607+
var source = {a1: 1, b1: {b2: {b3: 1}}, c1: [1, {c2: 1}], d1: {d2: 1}};
608+
var dest;
609+
610+
dest = copy(source, {}, 1);
611+
expect(dest).toEqual({a1:1, b1:'...', c1:'...', d1:'...'});
612+
613+
dest = copy(source, {}, 2);
614+
expect(dest).toEqual({a1:1, b1:{b2:'...'}, c1:[1,'...'], d1:{d2:1}});
615+
616+
dest = copy(source, {}, 3);
617+
expect(dest).toEqual({a1: 1, b1: {b2: {b3: 1}}, c1: [1, {c2: 1}], d1: {d2: 1}});
618+
619+
dest = copy(source, {}, 4);
620+
expect(dest).toEqual({a1: 1, b1: {b2: {b3: 1}}, c1: [1, {c2: 1}], d1: {d2: 1}});
621+
});
622+
623+
they('should copy source and ignore max depth when maxDepth = $prop',
624+
[NaN, null, undefined, true, false, -1, 0], function(maxDepth) {
625+
var source = {a1: 1, b1: {b2: {b3: 1}}, c1: [1, {c2: 1}], d1: {d2: 1}};
626+
var dest = copy(source, {}, maxDepth);
627+
expect(dest).toEqual({a1: 1, b1: {b2: {b3: 1}}, c1: [1, {c2: 1}], d1: {d2: 1}});
628+
}
629+
);
605630
});
606631

607632
describe('extend', function() {

test/minErrSpec.js

+39
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ describe('minErr', function() {
99
var emptyTestError = minErr(),
1010
testError = minErr('test');
1111

12+
var originalObjectMaxDepthInErrorMessage = minErrConfig.objectMaxDepth;
13+
afterEach(function() {
14+
minErrConfig.objectMaxDepth = originalObjectMaxDepthInErrorMessage;
15+
});
16+
1217
it('should return an Error factory', function() {
1318
var myError = testError('test', 'Oops');
1419
expect(myError instanceof Error).toBe(true);
@@ -68,6 +73,40 @@ describe('minErr', function() {
6873
expect(myError.message).toMatch(/a is {"b":{"a":"..."}}/);
6974
});
7075

76+
it('should handle arguments that are objects with max depth', function() {
77+
var a = {b: {c: {d: {e: {f: {g: 1}}}}}};
78+
79+
var myError = testError('26', 'a when objectMaxDepth is default=5 is {0}', a);
80+
expect(myError.message).toMatch(/a when objectMaxDepth is default=5 is {"b":{"c":{"d":{"e":{"f":"..."}}}}}/);
81+
expect(errorHandlingConfig().objectMaxDepth).toBe(5);
82+
83+
errorHandlingConfig({objectMaxDepth: 1});
84+
myError = testError('26', 'a when objectMaxDepth is set to 1 is {0}', a);
85+
expect(myError.message).toMatch(/a when objectMaxDepth is set to 1 is {"b":"..."}/);
86+
expect(errorHandlingConfig().objectMaxDepth).toBe(1);
87+
88+
errorHandlingConfig({objectMaxDepth: 2});
89+
myError = testError('26', 'a when objectMaxDepth is set to 2 is {0}', a);
90+
expect(myError.message).toMatch(/a when objectMaxDepth is set to 2 is {"b":{"c":"..."}}/);
91+
expect(errorHandlingConfig().objectMaxDepth).toBe(2);
92+
93+
errorHandlingConfig({objectMaxDepth: undefined});
94+
myError = testError('26', 'a when objectMaxDepth is set to undefined is {0}', a);
95+
expect(myError.message).toMatch(/a when objectMaxDepth is set to undefined is {"b":{"c":"..."}}/);
96+
expect(errorHandlingConfig().objectMaxDepth).toBe(2);
97+
});
98+
99+
they('should handle arguments that are objects and ignore max depth when objectMaxDepth = $prop',
100+
[NaN, null, true, false, -1, 0], function(maxDepth) {
101+
var a = {b: {c: {d: 1}}};
102+
103+
errorHandlingConfig({objectMaxDepth: maxDepth});
104+
var myError = testError('26', 'a is {0}', a);
105+
expect(myError.message).toMatch(/a is {"b":{"c":{"d":1}}}/);
106+
expect(errorHandlingConfig().objectMaxDepth).toBeNaN();
107+
}
108+
);
109+
71110
it('should preserve interpolation markers when fewer arguments than needed are provided', function() {
72111
// this way we can easily see if we are passing fewer args than needed
73112

test/stringifySpec.js

+39
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,43 @@ describe('toDebugString', function() {
1212
expect(toDebugString(a)).toEqual('{"a":"..."}');
1313
expect(toDebugString([a,a])).toEqual('[{"a":"..."},"..."]');
1414
});
15+
16+
it('should convert its argument that are objects to string based on maxDepth', function() {
17+
var a = {b: {c: {d: 1}}};
18+
expect(toDebugString(a, 1)).toEqual('{"b":"..."}');
19+
expect(toDebugString(a, 2)).toEqual('{"b":{"c":"..."}}');
20+
expect(toDebugString(a, 3)).toEqual('{"b":{"c":{"d":1}}}');
21+
});
22+
23+
they('should convert its argument that object to string and ignore max depth when maxDepth = $prop',
24+
[NaN, null, undefined, true, false, -1, 0], function(maxDepth) {
25+
var a = {b: {c: {d: 1}}};
26+
expect(toDebugString(a, maxDepth)).toEqual('{"b":{"c":{"d":1}}}');
27+
}
28+
);
29+
});
30+
31+
describe('serializeObject', function() {
32+
it('should convert its argument to a string', function() {
33+
expect(serializeObject({a:{b:'c'}})).toEqual('{"a":{"b":"c"}}');
34+
35+
var a = { };
36+
a.a = a;
37+
expect(serializeObject(a)).toEqual('{"a":"..."}');
38+
expect(serializeObject([a,a])).toEqual('[{"a":"..."},"..."]');
39+
});
40+
41+
it('should convert its argument that are objects to string based on maxDepth', function() {
42+
var a = {b: {c: {d: 1}}};
43+
expect(serializeObject(a, 1)).toEqual('{"b":"..."}');
44+
expect(serializeObject(a, 2)).toEqual('{"b":{"c":"..."}}');
45+
expect(serializeObject(a, 3)).toEqual('{"b":{"c":{"d":1}}}');
46+
});
47+
48+
they('should convert its argument that object to string and ignore max depth when maxDepth = $prop',
49+
[NaN, null, undefined, true, false, -1, 0], function(maxDepth) {
50+
var a = {b: {c: {d: 1}}};
51+
expect(serializeObject(a, maxDepth)).toEqual('{"b":{"c":{"d":1}}}');
52+
}
53+
);
1554
});

0 commit comments

Comments
 (0)