Skip to content

Commit

Permalink
util: include reference anchor for circular structures
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
BridgeAR committed May 20, 2019
1 parent 5518664 commit 9f71dbc
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 29 deletions.
23 changes: 21 additions & 2 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,9 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/27685
description: Circular references now include a marker to the reference.
- version: v12.0.0
pr-url: https://github.com/nodejs/node/pull/27109
description: The `compact` options default is changed to `3` and the
Expand Down Expand Up @@ -514,6 +517,24 @@ util.inspect(new Bar()); // 'Bar {}'
util.inspect(baz); // '[foo] {}'
```

Circular references point to their anchor by using a reference index:

```js
const { inspect } = require('util');

const obj = {};
obj.a = [obj];
obj.b = {};
obj.b.inner = obj.b;
obj.b.obj = obj;

console.log(inspect(obj));
// <ref *1> {
// a: [ [Circular *1] ],
// b: <ref *2> { inner: [Circular *2], obj: [Circular *1] }
// }
```

The following example inspects all properties of the `util` object:

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

// This will print

// { a:
// [ 1,
// 2,
Expand Down
27 changes: 25 additions & 2 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,8 +563,19 @@ function formatValue(ctx, value, recurseTimes, typedArray) {

// Using an array here is actually better for the average case than using
// a Set. `seen` will only check for the depth and will never grow too large.
if (ctx.seen.includes(value))
return ctx.stylize('[Circular]', 'special');
if (ctx.seen.includes(value)) {
let index = 1;
if (ctx.circular === undefined) {
ctx.circular = new Map([[value, index]]);
} else {
index = ctx.circular.get(value);
if (index === undefined) {
index = ctx.circular.size + 1;
ctx.circular.set(value, index);
}
}
return ctx.stylize(`[Circular *${index}]`, 'special');
}

return formatRaw(ctx, value, recurseTimes, typedArray);
}
Expand Down Expand Up @@ -766,6 +777,18 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
const constructorName = getCtxStyle(value, constructor, tag).slice(0, -1);
return handleMaxCallStackSize(ctx, err, constructorName, indentationLvl);
}
if (ctx.circular !== undefined) {
const index = ctx.circular.get(value);
if (index !== undefined) {
const reference = ctx.stylize(`<ref *${index}>`, 'special');
// Add reference always to the very beginning of the output.
if (ctx.compact !== true) {
base = base === '' ? reference : `${reference} ${base}`;
} else {
braces[0] = `${reference} ${braces[0]}`;
}
}
}
ctx.seen.pop();

if (ctx.sorted) {
Expand Down
3 changes: 2 additions & 1 deletion test/parallel/test-assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,8 @@ testAssertionMessage({}, '{}');
testAssertionMessage([1, 2, 3], '[\n+ 1,\n+ 2,\n+ 3\n+ ]');
testAssertionMessage(function f() {}, '[Function: f]');
testAssertionMessage(function() {}, '[Function (anonymous)]');
testAssertionMessage(circular, '{\n+ x: [Circular],\n+ y: 1\n+ }');
testAssertionMessage(circular,
'<ref *1> {\n+ x: [Circular *1],\n+ y: 1\n+ }');
testAssertionMessage({ a: undefined, b: null },
'{\n+ a: undefined,\n+ b: null\n+ }');
testAssertionMessage({ a: NaN, b: Infinity, c: -Infinity },
Expand Down
24 changes: 12 additions & 12 deletions test/parallel/test-util-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,10 @@ assert.strictEqual(
'{\n' +
' foo: \'bar\',\n' +
' foobar: 1,\n' +
' func: [Function: func] {\n' +
' func: <ref *1> [Function: func] {\n' +
' [length]: 0,\n' +
' [name]: \'func\',\n' +
' [prototype]: func { [constructor]: [Circular] }\n' +
' [prototype]: func { [constructor]: [Circular *1] }\n' +
' }\n' +
'}');
assert.strictEqual(
Expand All @@ -208,10 +208,10 @@ assert.strictEqual(
' foobar: 1,\n' +
' func: [\n' +
' {\n' +
' a: [Function: a] {\n' +
' a: <ref *1> [Function: a] {\n' +
' [length]: 0,\n' +
' [name]: \'a\',\n' +
' [prototype]: a { [constructor]: [Circular] }\n' +
' [prototype]: a { [constructor]: [Circular *1] }\n' +
' }\n' +
' },\n' +
' [length]: 1\n' +
Expand All @@ -223,10 +223,10 @@ assert.strictEqual(
' foo: \'bar\',\n' +
' foobar: {\n' +
' foo: \'bar\',\n' +
' func: [Function: func] {\n' +
' func: <ref *1> [Function: func] {\n' +
' [length]: 0,\n' +
' [name]: \'func\',\n' +
' [prototype]: func { [constructor]: [Circular] }\n' +
' [prototype]: func { [constructor]: [Circular *1] }\n' +
' }\n' +
' }\n' +
'}');
Expand All @@ -235,29 +235,29 @@ assert.strictEqual(
'{\n' +
' foo: \'bar\',\n' +
' foobar: 1,\n' +
' func: [Function: func] {\n' +
' func: <ref *1> [Function: func] {\n' +
' [length]: 0,\n' +
' [name]: \'func\',\n' +
' [prototype]: func { [constructor]: [Circular] }\n' +
' [prototype]: func { [constructor]: [Circular *1] }\n' +
' }\n' +
'} {\n' +
' foo: \'bar\',\n' +
' foobar: 1,\n' +
' func: [Function: func] {\n' +
' func: <ref *1> [Function: func] {\n' +
' [length]: 0,\n' +
' [name]: \'func\',\n' +
' [prototype]: func { [constructor]: [Circular] }\n' +
' [prototype]: func { [constructor]: [Circular *1] }\n' +
' }\n' +
'}');
assert.strictEqual(
util.format('%o %o', obj),
'{\n' +
' foo: \'bar\',\n' +
' foobar: 1,\n' +
' func: [Function: func] {\n' +
' func: <ref *1> [Function: func] {\n' +
' [length]: 0,\n' +
' [name]: \'func\',\n' +
' [prototype]: func { [constructor]: [Circular] }\n' +
' [prototype]: func { [constructor]: [Circular *1] }\n' +
' }\n' +
'} %o');

Expand Down
46 changes: 34 additions & 12 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ assert.strictEqual(

const value = {};
value.a = value;
assert.strictEqual(util.inspect(value), '{ a: [Circular] }');
assert.strictEqual(util.inspect(value), '<ref *1> { a: [Circular *1] }');
}

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

// Test Map.
Expand All @@ -1011,12 +1011,32 @@ if (typeof Symbol !== 'undefined') {
{
const map = new Map();
map.set(map, 'map');
assert.strictEqual(util.inspect(map), "Map { [Circular] => 'map' }");
assert.strictEqual(inspect(map), "<ref *1> Map { [Circular *1] => 'map' }");
map.set(map, map);
assert.strictEqual(util.inspect(map), 'Map { [Circular] => [Circular] }');
assert.strictEqual(
inspect(map),
'<ref *1> Map { [Circular *1] => [Circular *1] }'
);
map.delete(map);
map.set('map', map);
assert.strictEqual(util.inspect(map), "Map { 'map' => [Circular] }");
assert.strictEqual(inspect(map), "<ref *1> Map { 'map' => [Circular *1] }");
}

// Test multiple circular references.
{
const obj = {};
obj.a = [obj];
obj.b = {};
obj.b.inner = obj.b;
obj.b.obj = obj;

assert.strictEqual(
inspect(obj),
'<ref *1> {\n' +
' a: [ [Circular *1] ],\n' +
' b: <ref *2> { inner: [Circular *2], obj: [Circular *1] }\n' +
'}'
);
}

// Test Promise.
Expand Down Expand Up @@ -1214,7 +1234,9 @@ if (typeof Symbol !== 'undefined') {
arr[0][0][0] = { a: 2 };
assert.strictEqual(util.inspect(arr), '[ [ [ [Object] ] ] ]');
arr[0][0][0] = arr;
assert.strictEqual(util.inspect(arr), '[ [ [ [Circular] ] ] ]');
assert.strictEqual(util.inspect(arr), '<ref *1> [ [ [ [Circular *1] ] ] ]');
arr[0][0][0] = arr[0][0];
assert.strictEqual(util.inspect(arr), '[ [ <ref *1> [ [Circular *1] ] ] ]');
}

// Corner cases.
Expand Down Expand Up @@ -1608,7 +1630,7 @@ util.inspect(process);
' 2,',
' [length]: 2',
' ]',
' } => [Map Iterator] {',
' } => <ref *1> [Map Iterator] {',
' Uint8Array [',
' [BYTES_PER_ELEMENT]: 1,',
' [length]: 0,',
Expand All @@ -1619,7 +1641,7 @@ util.inspect(process);
' foo: true',
' }',
' ],',
' [Circular]',
' [Circular *1]',
' },',
' [size]: 2',
'}'
Expand Down Expand Up @@ -1647,15 +1669,15 @@ util.inspect(process);
' [byteOffset]: 0,',
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
' ],',
' [Set Iterator] { [ 1, 2, [length]: 2 ] } => [Map Iterator] {',
' [Set Iterator] { [ 1, 2, [length]: 2 ] } => <ref *1> [Map Iterator] {',
' Uint8Array [',
' [BYTES_PER_ELEMENT]: 1,',
' [length]: 0,',
' [byteLength]: 0,',
' [byteOffset]: 0,',
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
' ],',
' [Circular]',
' [Circular *1]',
' },',
' [size]: 2',
'}'
Expand Down Expand Up @@ -1687,7 +1709,7 @@ util.inspect(process);
' [Set Iterator] {',
' [ 1,',
' 2,',
' [length]: 2 ] } => [Map Iterator] {',
' [length]: 2 ] } => <ref *1> [Map Iterator] {',
' Uint8Array [',
' [BYTES_PER_ELEMENT]: 1,',
' [length]: 0,',
Expand All @@ -1696,7 +1718,7 @@ util.inspect(process);
' [buffer]: ArrayBuffer {',
' byteLength: 0,',
' foo: true } ],',
' [Circular] },',
' [Circular *1] },',
' [size]: 2 }'
].join('\n');

Expand Down

0 comments on commit 9f71dbc

Please sign in to comment.