Skip to content

Commit 33552f2

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. PR-URL: nodejs#27685 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: Anto Aravinth <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]>
1 parent 7d9eb17 commit 33552f2

File tree

5 files changed

+93
-28
lines changed

5 files changed

+93
-28
lines changed

doc/api/util.md

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

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

518539
```js
@@ -536,8 +557,6 @@ const o = {
536557
};
537558
console.log(util.inspect(o, { compact: true, depth: 5, breakLength: 80 }));
538559

539-
// This will print
540-
541560
// { a:
542561
// [ 1,
543562
// 2,

lib/internal/util/inspect.js

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

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

567578
return formatRaw(ctx, value, recurseTimes, typedArray);
568579
}
@@ -764,6 +775,18 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
764775
const constructorName = getCtxStyle(value, constructor, tag).slice(0, -1);
765776
return handleMaxCallStackSize(ctx, err, constructorName, indentationLvl);
766777
}
778+
if (ctx.circular !== undefined) {
779+
const index = ctx.circular.get(value);
780+
if (index !== undefined) {
781+
const reference = ctx.stylize(`<ref *${index}>`, 'special');
782+
// Add reference always to the very beginning of the output.
783+
if (ctx.compact !== true) {
784+
base = base === '' ? reference : `${reference} ${base}`;
785+
} else {
786+
braces[0] = `${reference} ${braces[0]}`;
787+
}
788+
}
789+
}
767790
ctx.seen.pop();
768791

769792
if (ctx.sorted) {

test/parallel/test-assert.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,8 @@ testAssertionMessage(/abc/gim, '/abc/gim');
342342
testAssertionMessage({}, '{}');
343343
testAssertionMessage([1, 2, 3], '[\n+ 1,\n+ 2,\n+ 3\n+ ]');
344344
testAssertionMessage(function f() {}, '[Function: f]');
345-
testAssertionMessage(circular, '{\n+ x: [Circular],\n+ y: 1\n+ }');
345+
testAssertionMessage(circular,
346+
'<ref *1> {\n+ x: [Circular *1],\n+ y: 1\n+ }');
346347
testAssertionMessage({ a: undefined, b: null },
347348
'{\n+ a: undefined,\n+ b: null\n+ }');
348349
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
@@ -195,10 +195,10 @@ assert.strictEqual(
195195
'{\n' +
196196
' foo: \'bar\',\n' +
197197
' foobar: 1,\n' +
198-
' func: [Function: func] {\n' +
198+
' func: <ref *1> [Function: func] {\n' +
199199
' [length]: 0,\n' +
200200
' [name]: \'func\',\n' +
201-
' [prototype]: func { [constructor]: [Circular] }\n' +
201+
' [prototype]: func { [constructor]: [Circular *1] }\n' +
202202
' }\n' +
203203
'}');
204204
assert.strictEqual(
@@ -208,10 +208,10 @@ assert.strictEqual(
208208
' foobar: 1,\n' +
209209
' func: [\n' +
210210
' {\n' +
211-
' a: [Function: a] {\n' +
211+
' a: <ref *1> [Function: a] {\n' +
212212
' [length]: 0,\n' +
213213
' [name]: \'a\',\n' +
214-
' [prototype]: a { [constructor]: [Circular] }\n' +
214+
' [prototype]: a { [constructor]: [Circular *1] }\n' +
215215
' }\n' +
216216
' },\n' +
217217
' [length]: 1\n' +
@@ -223,10 +223,10 @@ assert.strictEqual(
223223
' foo: \'bar\',\n' +
224224
' foobar: {\n' +
225225
' foo: \'bar\',\n' +
226-
' func: [Function: func] {\n' +
226+
' func: <ref *1> [Function: func] {\n' +
227227
' [length]: 0,\n' +
228228
' [name]: \'func\',\n' +
229-
' [prototype]: func { [constructor]: [Circular] }\n' +
229+
' [prototype]: func { [constructor]: [Circular *1] }\n' +
230230
' }\n' +
231231
' }\n' +
232232
'}');
@@ -235,29 +235,29 @@ assert.strictEqual(
235235
'{\n' +
236236
' foo: \'bar\',\n' +
237237
' foobar: 1,\n' +
238-
' func: [Function: func] {\n' +
238+
' func: <ref *1> [Function: func] {\n' +
239239
' [length]: 0,\n' +
240240
' [name]: \'func\',\n' +
241-
' [prototype]: func { [constructor]: [Circular] }\n' +
241+
' [prototype]: func { [constructor]: [Circular *1] }\n' +
242242
' }\n' +
243243
'} {\n' +
244244
' foo: \'bar\',\n' +
245245
' foobar: 1,\n' +
246-
' func: [Function: func] {\n' +
246+
' func: <ref *1> [Function: func] {\n' +
247247
' [length]: 0,\n' +
248248
' [name]: \'func\',\n' +
249-
' [prototype]: func { [constructor]: [Circular] }\n' +
249+
' [prototype]: func { [constructor]: [Circular *1] }\n' +
250250
' }\n' +
251251
'}');
252252
assert.strictEqual(
253253
util.format('%o %o', obj),
254254
'{\n' +
255255
' foo: \'bar\',\n' +
256256
' foobar: 1,\n' +
257-
' func: [Function: func] {\n' +
257+
' func: <ref *1> [Function: func] {\n' +
258258
' [length]: 0,\n' +
259259
' [name]: \'func\',\n' +
260-
' [prototype]: func { [constructor]: [Circular] }\n' +
260+
' [prototype]: func { [constructor]: [Circular *1] }\n' +
261261
' }\n' +
262262
'} %o');
263263

test/parallel/test-util-inspect.js

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,7 @@ if (typeof Symbol !== 'undefined') {
10211021
{
10221022
const set = new Set();
10231023
set.add(set);
1024-
assert.strictEqual(util.inspect(set), 'Set { [Circular] }');
1024+
assert.strictEqual(util.inspect(set), '<ref *1> Set { [Circular *1] }');
10251025
}
10261026

10271027
// Test Map.
@@ -1039,12 +1039,32 @@ if (typeof Symbol !== 'undefined') {
10391039
{
10401040
const map = new Map();
10411041
map.set(map, 'map');
1042-
assert.strictEqual(util.inspect(map), "Map { [Circular] => 'map' }");
1042+
assert.strictEqual(inspect(map), "<ref *1> Map { [Circular *1] => 'map' }");
10431043
map.set(map, map);
1044-
assert.strictEqual(util.inspect(map), 'Map { [Circular] => [Circular] }');
1044+
assert.strictEqual(
1045+
inspect(map),
1046+
'<ref *1> Map { [Circular *1] => [Circular *1] }'
1047+
);
10451048
map.delete(map);
10461049
map.set('map', map);
1047-
assert.strictEqual(util.inspect(map), "Map { 'map' => [Circular] }");
1050+
assert.strictEqual(inspect(map), "<ref *1> Map { 'map' => [Circular *1] }");
1051+
}
1052+
1053+
// Test multiple circular references.
1054+
{
1055+
const obj = {};
1056+
obj.a = [obj];
1057+
obj.b = {};
1058+
obj.b.inner = obj.b;
1059+
obj.b.obj = obj;
1060+
1061+
assert.strictEqual(
1062+
inspect(obj),
1063+
'<ref *1> {\n' +
1064+
' a: [ [Circular *1] ],\n' +
1065+
' b: <ref *2> { inner: [Circular *2], obj: [Circular *1] }\n' +
1066+
'}'
1067+
);
10481068
}
10491069

10501070
// Test Promise.
@@ -1242,7 +1262,9 @@ if (typeof Symbol !== 'undefined') {
12421262
arr[0][0][0] = { a: 2 };
12431263
assert.strictEqual(util.inspect(arr), '[ [ [ [Object] ] ] ]');
12441264
arr[0][0][0] = arr;
1245-
assert.strictEqual(util.inspect(arr), '[ [ [ [Circular] ] ] ]');
1265+
assert.strictEqual(util.inspect(arr), '<ref *1> [ [ [ [Circular *1] ] ] ]');
1266+
arr[0][0][0] = arr[0][0];
1267+
assert.strictEqual(util.inspect(arr), '[ [ <ref *1> [ [Circular *1] ] ] ]');
12461268
}
12471269

12481270
// Corner cases.
@@ -1609,7 +1631,7 @@ util.inspect(process);
16091631
' 2,',
16101632
' [length]: 2',
16111633
' ]',
1612-
' } => [Map Iterator] {',
1634+
' } => <ref *1> [Map Iterator] {',
16131635
' Uint8Array [',
16141636
' [BYTES_PER_ELEMENT]: 1,',
16151637
' [length]: 0,',
@@ -1620,7 +1642,7 @@ util.inspect(process);
16201642
' foo: true',
16211643
' }',
16221644
' ],',
1623-
' [Circular]',
1645+
' [Circular *1]',
16241646
' },',
16251647
' [size]: 2',
16261648
'}'
@@ -1648,15 +1670,15 @@ util.inspect(process);
16481670
' [byteOffset]: 0,',
16491671
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
16501672
' ],',
1651-
' [Set Iterator] { [ 1, 2, [length]: 2 ] } => [Map Iterator] {',
1673+
' [Set Iterator] { [ 1, 2, [length]: 2 ] } => <ref *1> [Map Iterator] {',
16521674
' Uint8Array [',
16531675
' [BYTES_PER_ELEMENT]: 1,',
16541676
' [length]: 0,',
16551677
' [byteLength]: 0,',
16561678
' [byteOffset]: 0,',
16571679
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
16581680
' ],',
1659-
' [Circular]',
1681+
' [Circular *1]',
16601682
' },',
16611683
' [size]: 2',
16621684
'}'
@@ -1688,7 +1710,7 @@ util.inspect(process);
16881710
' [Set Iterator] {',
16891711
' [ 1,',
16901712
' 2,',
1691-
' [length]: 2 ] } => [Map Iterator] {',
1713+
' [length]: 2 ] } => <ref *1> [Map Iterator] {',
16921714
' Uint8Array [',
16931715
' [BYTES_PER_ELEMENT]: 1,',
16941716
' [length]: 0,',
@@ -1697,7 +1719,7 @@ util.inspect(process);
16971719
' [buffer]: ArrayBuffer {',
16981720
' byteLength: 0,',
16991721
' foo: true } ],',
1700-
' [Circular] },',
1722+
' [Circular *1] },',
17011723
' [size]: 2 }'
17021724
].join('\n');
17031725

0 commit comments

Comments
 (0)