Skip to content

Commit 9f71dbc

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: #27685 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Anto Aravinth <anto.aravinth.cse@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
1 parent 5518664 commit 9f71dbc

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
@@ -392,6 +392,9 @@ stream.write('With ES6');
392392
<!-- YAML
393393
added: v0.3.0
394394
changes:
395+
- version: REPLACEME
396+
pr-url: https://github.com/nodejs/node/pull/27685
397+
description: Circular references now include a marker to the reference.
395398
- version: v12.0.0
396399
pr-url: https://github.com/nodejs/node/pull/27109
397400
description: The `compact` options default is changed to `3` and the
@@ -514,6 +517,24 @@ util.inspect(new Bar()); // 'Bar {}'
514517
util.inspect(baz); // '[foo] {}'
515518
```
516519

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

519540
```js
@@ -537,8 +558,6 @@ const o = {
537558
};
538559
console.log(util.inspect(o, { compact: true, depth: 5, breakLength: 80 }));
539560

540-
// This will print
541-
542561
// { a:
543562
// [ 1,
544563
// 2,

lib/internal/util/inspect.js

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

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

569580
return formatRaw(ctx, value, recurseTimes, typedArray);
570581
}
@@ -766,6 +777,18 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
766777
const constructorName = getCtxStyle(value, constructor, tag).slice(0, -1);
767778
return handleMaxCallStackSize(ctx, err, constructorName, indentationLvl);
768779
}
780+
if (ctx.circular !== undefined) {
781+
const index = ctx.circular.get(value);
782+
if (index !== undefined) {
783+
const reference = ctx.stylize(`<ref *${index}>`, 'special');
784+
// Add reference always to the very beginning of the output.
785+
if (ctx.compact !== true) {
786+
base = base === '' ? reference : `${reference} ${base}`;
787+
} else {
788+
braces[0] = `${reference} ${braces[0]}`;
789+
}
790+
}
791+
}
769792
ctx.seen.pop();
770793

771794
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
@@ -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: 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.
@@ -1214,7 +1234,9 @@ if (typeof Symbol !== 'undefined') {
12141234
arr[0][0][0] = { a: 2 };
12151235
assert.strictEqual(util.inspect(arr), '[ [ [ [Object] ] ] ]');
12161236
arr[0][0][0] = arr;
1217-
assert.strictEqual(util.inspect(arr), '[ [ [ [Circular] ] ] ]');
1237+
assert.strictEqual(util.inspect(arr), '<ref *1> [ [ [ [Circular *1] ] ] ]');
1238+
arr[0][0][0] = arr[0][0];
1239+
assert.strictEqual(util.inspect(arr), '[ [ <ref *1> [ [Circular *1] ] ] ]');
12181240
}
12191241

12201242
// Corner cases.
@@ -1608,7 +1630,7 @@ util.inspect(process);
16081630
' 2,',
16091631
' [length]: 2',
16101632
' ]',
1611-
' } => [Map Iterator] {',
1633+
' } => <ref *1> [Map Iterator] {',
16121634
' Uint8Array [',
16131635
' [BYTES_PER_ELEMENT]: 1,',
16141636
' [length]: 0,',
@@ -1619,7 +1641,7 @@ util.inspect(process);
16191641
' foo: true',
16201642
' }',
16211643
' ],',
1622-
' [Circular]',
1644+
' [Circular *1]',
16231645
' },',
16241646
' [size]: 2',
16251647
'}'
@@ -1647,15 +1669,15 @@ util.inspect(process);
16471669
' [byteOffset]: 0,',
16481670
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
16491671
' ],',
1650-
' [Set Iterator] { [ 1, 2, [length]: 2 ] } => [Map Iterator] {',
1672+
' [Set Iterator] { [ 1, 2, [length]: 2 ] } => <ref *1> [Map Iterator] {',
16511673
' Uint8Array [',
16521674
' [BYTES_PER_ELEMENT]: 1,',
16531675
' [length]: 0,',
16541676
' [byteLength]: 0,',
16551677
' [byteOffset]: 0,',
16561678
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
16571679
' ],',
1658-
' [Circular]',
1680+
' [Circular *1]',
16591681
' },',
16601682
' [size]: 2',
16611683
'}'
@@ -1687,7 +1709,7 @@ util.inspect(process);
16871709
' [Set Iterator] {',
16881710
' [ 1,',
16891711
' 2,',
1690-
' [length]: 2 ] } => [Map Iterator] {',
1712+
' [length]: 2 ] } => <ref *1> [Map Iterator] {',
16911713
' Uint8Array [',
16921714
' [BYTES_PER_ELEMENT]: 1,',
16931715
' [length]: 0,',
@@ -1696,7 +1718,7 @@ util.inspect(process);
16961718
' [buffer]: ArrayBuffer {',
16971719
' byteLength: 0,',
16981720
' foo: true } ],',
1699-
' [Circular] },',
1721+
' [Circular *1] },',
17001722
' [size]: 2 }'
17011723
].join('\n');
17021724

0 commit comments

Comments
 (0)