Skip to content

Commit 59adf42

Browse files
committed
util: include reference anchor for circular structures
This adds a reference anchor to circular structures when using `util.inspect`. That way it's possible to identify with what object the circular reference corresponds too.
1 parent 7294897 commit 59adf42

File tree

5 files changed

+94
-29
lines changed

5 files changed

+94
-29
lines changed

doc/api/util.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,9 @@ stream.write('With ES6');
390390
<!-- YAML
391391
added: v0.3.0
392392
changes:
393+
- version: REPLACEME
394+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
395+
description: Circular references now include a marker to the reference.
393396
- version: v12.0.0
394397
pr-url: https://github.com/nodejs/node/pull/27109
395398
description: The `compact` options default is changed to `3` and the
@@ -512,6 +515,24 @@ util.inspect(new Bar()); // 'Bar {}'
512515
util.inspect(baz); // '[foo] {}'
513516
```
514517

518+
Circular references point to their anchor by using a reference index:
519+
520+
```js
521+
const { inspect } = require('util');
522+
523+
const obj = {};
524+
obj.a = [obj];
525+
obj.b = {};
526+
obj.b.inner = obj.b;
527+
obj.b.obj = obj;
528+
529+
console.log(inspect(obj));
530+
// <ref *1> {
531+
// a: [ [Circular *1] ],
532+
// b: <ref *2> { inner: [Circular *2], obj: [Circular *1] }
533+
// }
534+
```
535+
515536
The following example inspects all properties of the `util` object:
516537

517538
```js
@@ -535,8 +556,6 @@ const o = {
535556
};
536557
console.log(util.inspect(o, { compact: true, depth: 5, breakLength: 80 }));
537558

538-
// This will print
539-
540559
// { a:
541560
// [ 1,
542561
// 2,

lib/internal/util/inspect.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,8 +557,19 @@ function formatValue(ctx, value, recurseTimes, typedArray) {
557557

558558
// Using an array here is actually better for the average case than using
559559
// a Set. `seen` will only check for the depth and will never grow too large.
560-
if (ctx.seen.includes(value))
561-
return ctx.stylize('[Circular]', 'special');
560+
if (ctx.seen.includes(value)) {
561+
let index = 1;
562+
if (ctx.circular === undefined) {
563+
ctx.circular = new Map([[value, index]]);
564+
} else {
565+
index = ctx.circular.get(value);
566+
if (index === undefined) {
567+
index = ctx.circular.size + 1;
568+
ctx.circular.set(value, index);
569+
}
570+
}
571+
return ctx.stylize(`[Circular *${index}]`, 'special');
572+
}
562573

563574
return formatRaw(ctx, value, recurseTimes, typedArray);
564575
}
@@ -755,6 +766,18 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
755766
} catch (err) {
756767
return handleMaxCallStackSize(ctx, err, constructor, tag, indentationLvl);
757768
}
769+
if (ctx.circular !== undefined) {
770+
const index = ctx.circular.get(value);
771+
if (index !== undefined) {
772+
const reference = ctx.stylize(`<ref *${index}>`, 'special');
773+
// Add reference always to the very beginning of the output.
774+
if (ctx.compact !== true) {
775+
base = base === '' ? reference : `${reference} ${base}`;
776+
} else {
777+
braces[0] = `${reference} ${braces[0]}`;
778+
}
779+
}
780+
}
758781
ctx.seen.pop();
759782

760783
if (ctx.sorted) {

test/parallel/test-assert.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,8 @@ testAssertionMessage({}, '{}');
298298
testAssertionMessage([1, 2, 3], '[\n+ 1,\n+ 2,\n+ 3\n+ ]');
299299
testAssertionMessage(function f() {}, '[Function: f]');
300300
testAssertionMessage(function() {}, '[Function (anonymous)]');
301-
testAssertionMessage(circular, '{\n+ x: [Circular],\n+ y: 1\n+ }');
301+
testAssertionMessage(circular,
302+
'<ref *1> {\n+ x: [Circular *1],\n+ y: 1\n+ }');
302303
testAssertionMessage({ a: undefined, b: null },
303304
'{\n+ a: undefined,\n+ b: null\n+ }');
304305
testAssertionMessage({ a: NaN, b: Infinity, c: -Infinity },

test/parallel/test-util-format.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,10 @@ assert.strictEqual(
173173
'{\n' +
174174
' foo: \'bar\',\n' +
175175
' foobar: 1,\n' +
176-
' func: [Function: func] {\n' +
176+
' func: <ref *1> [Function: func] {\n' +
177177
' [length]: 0,\n' +
178178
' [name]: \'func\',\n' +
179-
' [prototype]: func { [constructor]: [Circular] }\n' +
179+
' [prototype]: func { [constructor]: [Circular *1] }\n' +
180180
' }\n' +
181181
'}');
182182
assert.strictEqual(
@@ -186,10 +186,10 @@ assert.strictEqual(
186186
' foobar: 1,\n' +
187187
' func: [\n' +
188188
' {\n' +
189-
' a: [Function: a] {\n' +
189+
' a: <ref *1> [Function: a] {\n' +
190190
' [length]: 0,\n' +
191191
' [name]: \'a\',\n' +
192-
' [prototype]: a { [constructor]: [Circular] }\n' +
192+
' [prototype]: a { [constructor]: [Circular *1] }\n' +
193193
' }\n' +
194194
' },\n' +
195195
' [length]: 1\n' +
@@ -201,10 +201,10 @@ assert.strictEqual(
201201
' foo: \'bar\',\n' +
202202
' foobar: {\n' +
203203
' foo: \'bar\',\n' +
204-
' func: [Function: func] {\n' +
204+
' func: <ref *1> [Function: func] {\n' +
205205
' [length]: 0,\n' +
206206
' [name]: \'func\',\n' +
207-
' [prototype]: func { [constructor]: [Circular] }\n' +
207+
' [prototype]: func { [constructor]: [Circular *1] }\n' +
208208
' }\n' +
209209
' }\n' +
210210
'}');
@@ -213,29 +213,29 @@ assert.strictEqual(
213213
'{\n' +
214214
' foo: \'bar\',\n' +
215215
' foobar: 1,\n' +
216-
' func: [Function: func] {\n' +
216+
' func: <ref *1> [Function: func] {\n' +
217217
' [length]: 0,\n' +
218218
' [name]: \'func\',\n' +
219-
' [prototype]: func { [constructor]: [Circular] }\n' +
219+
' [prototype]: func { [constructor]: [Circular *1] }\n' +
220220
' }\n' +
221221
'} {\n' +
222222
' foo: \'bar\',\n' +
223223
' foobar: 1,\n' +
224-
' func: [Function: func] {\n' +
224+
' func: <ref *1> [Function: func] {\n' +
225225
' [length]: 0,\n' +
226226
' [name]: \'func\',\n' +
227-
' [prototype]: func { [constructor]: [Circular] }\n' +
227+
' [prototype]: func { [constructor]: [Circular *1] }\n' +
228228
' }\n' +
229229
'}');
230230
assert.strictEqual(
231231
util.format('%o %o', obj),
232232
'{\n' +
233233
' foo: \'bar\',\n' +
234234
' foobar: 1,\n' +
235-
' func: [Function: func] {\n' +
235+
' func: <ref *1> [Function: func] {\n' +
236236
' [length]: 0,\n' +
237237
' [name]: \'func\',\n' +
238-
' [prototype]: func { [constructor]: [Circular] }\n' +
238+
' [prototype]: func { [constructor]: [Circular *1] }\n' +
239239
' }\n' +
240240
'} %o');
241241

test/parallel/test-util-inspect.js

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ assert.strictEqual(
338338

339339
const value = {};
340340
value.a = value;
341-
assert.strictEqual(util.inspect(value), '{ a: [Circular] }');
341+
assert.strictEqual(util.inspect(value), '<ref *1> { a: [Circular *1] }');
342342
}
343343

344344
// Array with dynamic properties.
@@ -993,7 +993,7 @@ if (typeof Symbol !== 'undefined') {
993993
{
994994
const set = new Set();
995995
set.add(set);
996-
assert.strictEqual(util.inspect(set), 'Set { [Circular] }');
996+
assert.strictEqual(util.inspect(set), '<ref *1> Set { [Circular *1] }');
997997
}
998998

999999
// Test Map.
@@ -1011,12 +1011,32 @@ if (typeof Symbol !== 'undefined') {
10111011
{
10121012
const map = new Map();
10131013
map.set(map, 'map');
1014-
assert.strictEqual(util.inspect(map), "Map { [Circular] => 'map' }");
1014+
assert.strictEqual(inspect(map), "<ref *1> Map { [Circular *1] => 'map' }");
10151015
map.set(map, map);
1016-
assert.strictEqual(util.inspect(map), 'Map { [Circular] => [Circular] }');
1016+
assert.strictEqual(
1017+
inspect(map),
1018+
'<ref *1> Map { [Circular *1] => [Circular *1] }'
1019+
);
10171020
map.delete(map);
10181021
map.set('map', map);
1019-
assert.strictEqual(util.inspect(map), "Map { 'map' => [Circular] }");
1022+
assert.strictEqual(inspect(map), "<ref *1> Map { 'map' => [Circular *1] }");
1023+
}
1024+
1025+
// Test multiple circular references.
1026+
{
1027+
const obj = {};
1028+
obj.a = [obj];
1029+
obj.b = {};
1030+
obj.b.inner = obj.b;
1031+
obj.b.obj = obj;
1032+
1033+
assert.strictEqual(
1034+
inspect(obj),
1035+
'<ref *1> {\n' +
1036+
' a: [ [Circular *1] ],\n' +
1037+
' b: <ref *2> { inner: [Circular *2], obj: [Circular *1] }\n' +
1038+
'}'
1039+
);
10201040
}
10211041

10221042
// Test Promise.
@@ -1169,7 +1189,9 @@ if (typeof Symbol !== 'undefined') {
11691189
arr[0][0][0] = { a: 2 };
11701190
assert.strictEqual(util.inspect(arr), '[ [ [ [Object] ] ] ]');
11711191
arr[0][0][0] = arr;
1172-
assert.strictEqual(util.inspect(arr), '[ [ [ [Circular] ] ] ]');
1192+
assert.strictEqual(util.inspect(arr), '<ref *1> [ [ [ [Circular *1] ] ] ]');
1193+
arr[0][0][0] = arr[0][0];
1194+
assert.strictEqual(util.inspect(arr), '[ [ <ref *1> [ [Circular *1] ] ] ]');
11731195
}
11741196

11751197
// Corner cases.
@@ -1563,7 +1585,7 @@ util.inspect(process);
15631585
' 2,',
15641586
' [length]: 2',
15651587
' ]',
1566-
' } => [Map Iterator] {',
1588+
' } => <ref *1> [Map Iterator] {',
15671589
' Uint8Array [',
15681590
' [BYTES_PER_ELEMENT]: 1,',
15691591
' [length]: 0,',
@@ -1574,7 +1596,7 @@ util.inspect(process);
15741596
' foo: true',
15751597
' }',
15761598
' ],',
1577-
' [Circular]',
1599+
' [Circular *1]',
15781600
' },',
15791601
' [size]: 2',
15801602
'}'
@@ -1602,15 +1624,15 @@ util.inspect(process);
16021624
' [byteOffset]: 0,',
16031625
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
16041626
' ],',
1605-
' [Set Iterator] { [ 1, 2, [length]: 2 ] } => [Map Iterator] {',
1627+
' [Set Iterator] { [ 1, 2, [length]: 2 ] } => <ref *1> [Map Iterator] {',
16061628
' Uint8Array [',
16071629
' [BYTES_PER_ELEMENT]: 1,',
16081630
' [length]: 0,',
16091631
' [byteLength]: 0,',
16101632
' [byteOffset]: 0,',
16111633
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
16121634
' ],',
1613-
' [Circular]',
1635+
' [Circular *1]',
16141636
' },',
16151637
' [size]: 2',
16161638
'}'
@@ -1642,7 +1664,7 @@ util.inspect(process);
16421664
' [Set Iterator] {',
16431665
' [ 1,',
16441666
' 2,',
1645-
' [length]: 2 ] } => [Map Iterator] {',
1667+
' [length]: 2 ] } => <ref *1> [Map Iterator] {',
16461668
' Uint8Array [',
16471669
' [BYTES_PER_ELEMENT]: 1,',
16481670
' [length]: 0,',
@@ -1651,7 +1673,7 @@ util.inspect(process);
16511673
' [buffer]: ArrayBuffer {',
16521674
' byteLength: 0,',
16531675
' foo: true } ],',
1654-
' [Circular] },',
1676+
' [Circular *1] },',
16551677
' [size]: 2 }'
16561678
].join('\n');
16571679

0 commit comments

Comments
 (0)