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 <!-- YAML
added: v0.3.0 added: v0.3.0
changes: 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 - version: v12.0.0
pr-url: https://github.com/nodejs/node/pull/27109 pr-url: https://github.com/nodejs/node/pull/27109
description: The `compact` options default is changed to `3` and the description: The `compact` options default is changed to `3` and the
@ -514,6 +517,24 @@ util.inspect(new Bar()); // 'Bar {}'
util.inspect(baz); // '[foo] {}' 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: The following example inspects all properties of the `util` object:
```js ```js
@ -537,8 +558,6 @@ const o = {
}; };
console.log(util.inspect(o, { compact: true, depth: 5, breakLength: 80 })); console.log(util.inspect(o, { compact: true, depth: 5, breakLength: 80 }));
// This will print
// { a: // { a:
// [ 1, // [ 1,
// 2, // 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 // 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. // a Set. `seen` will only check for the depth and will never grow too large.
if (ctx.seen.includes(value)) if (ctx.seen.includes(value)) {
return ctx.stylize('[Circular]', 'special'); 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); 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); const constructorName = getCtxStyle(value, constructor, tag).slice(0, -1);
return handleMaxCallStackSize(ctx, err, constructorName, indentationLvl); 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(); ctx.seen.pop();
if (ctx.sorted) { if (ctx.sorted) {

View File

@ -298,7 +298,8 @@ testAssertionMessage({}, '{}');
testAssertionMessage([1, 2, 3], '[\n+ 1,\n+ 2,\n+ 3\n+ ]'); testAssertionMessage([1, 2, 3], '[\n+ 1,\n+ 2,\n+ 3\n+ ]');
testAssertionMessage(function f() {}, '[Function: f]'); testAssertionMessage(function f() {}, '[Function: f]');
testAssertionMessage(function() {}, '[Function (anonymous)]'); 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 }, testAssertionMessage({ a: undefined, b: null },
'{\n+ a: undefined,\n+ b: null\n+ }'); '{\n+ a: undefined,\n+ b: null\n+ }');
testAssertionMessage({ a: NaN, b: Infinity, c: -Infinity }, testAssertionMessage({ a: NaN, b: Infinity, c: -Infinity },

View File

@ -195,10 +195,10 @@ assert.strictEqual(
'{\n' + '{\n' +
' foo: \'bar\',\n' + ' foo: \'bar\',\n' +
' foobar: 1,\n' + ' foobar: 1,\n' +
' func: [Function: func] {\n' + ' func: <ref *1> [Function: func] {\n' +
' [length]: 0,\n' + ' [length]: 0,\n' +
' [name]: \'func\',\n' + ' [name]: \'func\',\n' +
' [prototype]: func { [constructor]: [Circular] }\n' + ' [prototype]: func { [constructor]: [Circular *1] }\n' +
' }\n' + ' }\n' +
'}'); '}');
assert.strictEqual( assert.strictEqual(
@ -208,10 +208,10 @@ assert.strictEqual(
' foobar: 1,\n' + ' foobar: 1,\n' +
' func: [\n' + ' func: [\n' +
' {\n' + ' {\n' +
' a: [Function: a] {\n' + ' a: <ref *1> [Function: a] {\n' +
' [length]: 0,\n' + ' [length]: 0,\n' +
' [name]: \'a\',\n' + ' [name]: \'a\',\n' +
' [prototype]: a { [constructor]: [Circular] }\n' + ' [prototype]: a { [constructor]: [Circular *1] }\n' +
' }\n' + ' }\n' +
' },\n' + ' },\n' +
' [length]: 1\n' + ' [length]: 1\n' +
@ -223,10 +223,10 @@ assert.strictEqual(
' foo: \'bar\',\n' + ' foo: \'bar\',\n' +
' foobar: {\n' + ' foobar: {\n' +
' foo: \'bar\',\n' + ' foo: \'bar\',\n' +
' func: [Function: func] {\n' + ' func: <ref *1> [Function: func] {\n' +
' [length]: 0,\n' + ' [length]: 0,\n' +
' [name]: \'func\',\n' + ' [name]: \'func\',\n' +
' [prototype]: func { [constructor]: [Circular] }\n' + ' [prototype]: func { [constructor]: [Circular *1] }\n' +
' }\n' + ' }\n' +
' }\n' + ' }\n' +
'}'); '}');
@ -235,18 +235,18 @@ assert.strictEqual(
'{\n' + '{\n' +
' foo: \'bar\',\n' + ' foo: \'bar\',\n' +
' foobar: 1,\n' + ' foobar: 1,\n' +
' func: [Function: func] {\n' + ' func: <ref *1> [Function: func] {\n' +
' [length]: 0,\n' + ' [length]: 0,\n' +
' [name]: \'func\',\n' + ' [name]: \'func\',\n' +
' [prototype]: func { [constructor]: [Circular] }\n' + ' [prototype]: func { [constructor]: [Circular *1] }\n' +
' }\n' + ' }\n' +
'} {\n' + '} {\n' +
' foo: \'bar\',\n' + ' foo: \'bar\',\n' +
' foobar: 1,\n' + ' foobar: 1,\n' +
' func: [Function: func] {\n' + ' func: <ref *1> [Function: func] {\n' +
' [length]: 0,\n' + ' [length]: 0,\n' +
' [name]: \'func\',\n' + ' [name]: \'func\',\n' +
' [prototype]: func { [constructor]: [Circular] }\n' + ' [prototype]: func { [constructor]: [Circular *1] }\n' +
' }\n' + ' }\n' +
'}'); '}');
assert.strictEqual( assert.strictEqual(
@ -254,10 +254,10 @@ assert.strictEqual(
'{\n' + '{\n' +
' foo: \'bar\',\n' + ' foo: \'bar\',\n' +
' foobar: 1,\n' + ' foobar: 1,\n' +
' func: [Function: func] {\n' + ' func: <ref *1> [Function: func] {\n' +
' [length]: 0,\n' + ' [length]: 0,\n' +
' [name]: \'func\',\n' + ' [name]: \'func\',\n' +
' [prototype]: func { [constructor]: [Circular] }\n' + ' [prototype]: func { [constructor]: [Circular *1] }\n' +
' }\n' + ' }\n' +
'} %o'); '} %o');

View File

@ -338,7 +338,7 @@ assert.strictEqual(
const value = {}; const value = {};
value.a = 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. // Array with dynamic properties.
@ -993,7 +993,7 @@ if (typeof Symbol !== 'undefined') {
{ {
const set = new Set(); const set = new Set();
set.add(set); set.add(set);
assert.strictEqual(util.inspect(set), 'Set { [Circular] }'); assert.strictEqual(util.inspect(set), '<ref *1> Set { [Circular *1] }');
} }
// Test Map. // Test Map.
@ -1011,12 +1011,32 @@ if (typeof Symbol !== 'undefined') {
{ {
const map = new Map(); const map = new Map();
map.set(map, '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); 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.delete(map);
map.set('map', 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. // Test Promise.
@ -1214,7 +1234,9 @@ if (typeof Symbol !== 'undefined') {
arr[0][0][0] = { a: 2 }; arr[0][0][0] = { a: 2 };
assert.strictEqual(util.inspect(arr), '[ [ [ [Object] ] ] ]'); assert.strictEqual(util.inspect(arr), '[ [ [ [Object] ] ] ]');
arr[0][0][0] = arr; 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. // Corner cases.
@ -1608,7 +1630,7 @@ util.inspect(process);
' 2,', ' 2,',
' [length]: 2', ' [length]: 2',
' ]', ' ]',
' } => [Map Iterator] {', ' } => <ref *1> [Map Iterator] {',
' Uint8Array [', ' Uint8Array [',
' [BYTES_PER_ELEMENT]: 1,', ' [BYTES_PER_ELEMENT]: 1,',
' [length]: 0,', ' [length]: 0,',
@ -1619,7 +1641,7 @@ util.inspect(process);
' foo: true', ' foo: true',
' }', ' }',
' ],', ' ],',
' [Circular]', ' [Circular *1]',
' },', ' },',
' [size]: 2', ' [size]: 2',
'}' '}'
@ -1647,7 +1669,7 @@ util.inspect(process);
' [byteOffset]: 0,', ' [byteOffset]: 0,',
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }', ' [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 [', ' Uint8Array [',
' [BYTES_PER_ELEMENT]: 1,', ' [BYTES_PER_ELEMENT]: 1,',
' [length]: 0,', ' [length]: 0,',
@ -1655,7 +1677,7 @@ util.inspect(process);
' [byteOffset]: 0,', ' [byteOffset]: 0,',
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }', ' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
' ],', ' ],',
' [Circular]', ' [Circular *1]',
' },', ' },',
' [size]: 2', ' [size]: 2',
'}' '}'
@ -1687,7 +1709,7 @@ util.inspect(process);
' [Set Iterator] {', ' [Set Iterator] {',
' [ 1,', ' [ 1,',
' 2,', ' 2,',
' [length]: 2 ] } => [Map Iterator] {', ' [length]: 2 ] } => <ref *1> [Map Iterator] {',
' Uint8Array [', ' Uint8Array [',
' [BYTES_PER_ELEMENT]: 1,', ' [BYTES_PER_ELEMENT]: 1,',
' [length]: 0,', ' [length]: 0,',
@ -1696,7 +1718,7 @@ util.inspect(process);
' [buffer]: ArrayBuffer {', ' [buffer]: ArrayBuffer {',
' byteLength: 0,', ' byteLength: 0,',
' foo: true } ],', ' foo: true } ],',
' [Circular] },', ' [Circular *1] },',
' [size]: 2 }' ' [size]: 2 }'
].join('\n'); ].join('\n');