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: https://github.com/nodejs/node/pull/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>
This commit is contained in:
Ruben Bridgewater 2019-05-14 02:53:22 +02:00
parent 5518664d41
commit 9f71dbc334
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
5 changed files with 94 additions and 29 deletions

View File

@ -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
@ -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
@ -537,8 +558,6 @@ const o = {
};
console.log(util.inspect(o, { compact: true, depth: 5, breakLength: 80 }));
// This will print
// { a:
// [ 1,
// 2,

View File

@ -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);
}
@ -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) {

View File

@ -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 },

View File

@ -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(
@ -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' +
@ -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' +
'}');
@ -235,18 +235,18 @@ 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(
@ -254,10 +254,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' +
'} %o');

View File

@ -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.
@ -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.
@ -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.
@ -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.
@ -1608,7 +1630,7 @@ util.inspect(process);
' 2,',
' [length]: 2',
' ]',
' } => [Map Iterator] {',
' } => <ref *1> [Map Iterator] {',
' Uint8Array [',
' [BYTES_PER_ELEMENT]: 1,',
' [length]: 0,',
@ -1619,7 +1641,7 @@ util.inspect(process);
' foo: true',
' }',
' ],',
' [Circular]',
' [Circular *1]',
' },',
' [size]: 2',
'}'
@ -1647,7 +1669,7 @@ 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,',
@ -1655,7 +1677,7 @@ util.inspect(process);
' [byteOffset]: 0,',
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
' ],',
' [Circular]',
' [Circular *1]',
' },',
' [size]: 2',
'}'
@ -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,',
@ -1696,7 +1718,7 @@ util.inspect(process);
' [buffer]: ArrayBuffer {',
' byteLength: 0,',
' foo: true } ],',
' [Circular] },',
' [Circular *1] },',
' [size]: 2 }'
].join('\n');