Skip to content

Commit

Permalink
util: show Weak(Set|Map) entries in inspect
Browse files Browse the repository at this point in the history
This adds support for WeakMap and WeakSet entries in `util.inspect`.
The output is limited to a maximum entry length of `maxArrayLength`.

PR-URL: #19259
Fixes: #19001:
Reviewed-By: Yosuke Furukawa <yosuke.furukawa@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
BridgeAR committed Mar 25, 2018
1 parent 0fbd4b1 commit 1029dd3
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 7 deletions.
37 changes: 32 additions & 5 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,9 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/19259
description: WeakMap and WeakSet entries can now be inspected as well.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/17907
description: The `depth` default changed to Infinity.
Expand All @@ -369,7 +372,8 @@ changes:
* `object` {any} Any JavaScript primitive or Object.
* `options` {Object}
* `showHidden` {boolean} If `true`, the `object`'s non-enumerable symbols and
properties will be included in the formatted result. Defaults to `false`.
properties will be included in the formatted result as well as [`WeakMap`][]
and [`WeakSet`][] entries. Defaults to `false`.
* `colors` {boolean} If `true`, the output will be styled with ANSI color
codes. Defaults to `false`. Colors are customizable, see
[Customizing `util.inspect` colors][].
Expand All @@ -378,10 +382,14 @@ changes:
* `showProxy` {boolean} If `true`, then objects and functions that are
`Proxy` objects will be introspected to show their `target` and `handler`
objects. Defaults to `false`.
* `maxArrayLength` {number} Specifies the maximum number of array and
`TypedArray` elements to include when formatting. Defaults to `100`. Set to
`null` or `Infinity` to show all array elements. Set to `0` or negative to
show no array elements.
<!--
TODO(BridgeAR): Deprecate `maxArrayLength` and replace it with
`maxEntries`.
-->
* `maxArrayLength` {number} Specifies the maximum number of `Array`,
[`TypedArray`][], [`WeakMap`][] and [`WeakSet`][] elements to include when
formatting. Defaults to `100`. Set to `null` or `Infinity` to show all
elements. Set to `0` or negative to show no elements.
* `breakLength` {number} The length at which an object's keys are split
across multiple lines. Set to `Infinity` to format an object as a single
line. Defaults to 60 for legacy compatibility.
Expand Down Expand Up @@ -501,6 +509,25 @@ console.log(util.inspect(o, { compact: false, breakLength: 80 }));
// chunks.
```

Using the `showHidden` option allows to inspect [`WeakMap`][] and [`WeakSet`][]
entries. If there are more entries than `maxArrayLength`, there is no guarantee
which entries are displayed. That means retrieving the same ['WeakSet'][]
entries twice might actually result in a different output. Besides this any item
might be collected at any point of time by the garbage collector if there is no
strong reference left to that object. Therefore there is no guarantee to get a
reliable output.

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

const obj = { a: 1 };
const obj2 = { b: 2 };
const weakSet = new WeakSet([obj, obj2]);

console.log(inspect(weakSet, { showHidden: true }));
// WeakSet { { a: 1 }, { b: 2 } }
```

Please note that `util.inspect()` is a synchronous method that is mainly
intended as a debugging tool. Some input values can have a significant
performance overhead that can block the event loop. Use this function
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,8 @@
const v8 = NativeModule.require('internal/v8');
v8.previewMapIterator(new Map().entries());
v8.previewSetIterator(new Set().entries());
v8.previewWeakMap(new WeakMap(), 1);
v8.previewWeakSet(new WeakSet(), 1);
// Disable --allow_natives_syntax again unless it was explicitly
// specified on the command line.
const re = /^--allow[-_]natives[-_]syntax$/;
Expand Down
18 changes: 17 additions & 1 deletion lib/internal/v8.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,23 @@ function previewSetIterator(it) {
return %SetIteratorClone(it);
}

// Retrieve all WeakMap instance key / value pairs up to `max`. `max` limits the
// number of key / value pairs returned. Make sure it is a positive number,
// otherwise V8 aborts. Passing through `0` returns all elements.
function previewWeakMap(weakMap, max) {
return %GetWeakMapEntries(weakMap, max);
}

// Retrieve all WeakSet instance values up to `max`. `max` limits the
// number of key / value pairs returned. Make sure it is a positive number,
// otherwise V8 aborts. Passing through `0` returns all elements.
function previewWeakSet(weakSet, max) {
return %GetWeakSetValues(weakSet, max);
}

module.exports = {
previewMapIterator,
previewSetIterator
previewSetIterator,
previewWeakMap,
previewWeakSet
};
68 changes: 67 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ const { isBuffer } = require('buffer').Buffer;

const {
previewMapIterator,
previewSetIterator
previewSetIterator,
previewWeakMap,
previewWeakSet
} = require('internal/v8');

const {
Expand All @@ -54,6 +56,8 @@ const {
isPromise,
isSet,
isSetIterator,
isWeakMap,
isWeakSet,
isRegExp,
isDate,
isTypedArray
Expand Down Expand Up @@ -291,6 +295,8 @@ function inspect(value, opts) {
colors: inspectDefaultOptions.colors,
customInspect: inspectDefaultOptions.customInspect,
showProxy: inspectDefaultOptions.showProxy,
// TODO(BridgeAR): Deprecate `maxArrayLength` and replace it with
// `maxEntries`.
maxArrayLength: inspectDefaultOptions.maxArrayLength,
breakLength: inspectDefaultOptions.breakLength,
indentationLvl: 0,
Expand Down Expand Up @@ -328,6 +334,8 @@ Object.defineProperty(inspect, 'defaultOptions', {
if (options === null || typeof options !== 'object') {
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
}
// TODO(BridgeAR): Add input validation and make sure `defaultOptions` are
// not configurable.
return _extend(inspectDefaultOptions, options);
}
});
Expand Down Expand Up @@ -465,6 +473,7 @@ function formatValue(ctx, value, recurseTimes, ln) {
let braces;
let noIterator = true;
let raw;
let extra;

// Iterators and the rest are split to reduce checks
if (value[Symbol.iterator]) {
Expand Down Expand Up @@ -562,6 +571,20 @@ function formatValue(ctx, value, recurseTimes, ln) {
} else if (isPromise(value)) {
braces[0] = `${prefix}{`;
formatter = formatPromise;
} else if (isWeakSet(value)) {
braces[0] = `${prefix}{`;
if (ctx.showHidden) {
formatter = formatWeakSet;
} else {
extra = '[items unknown]';
}
} else if (isWeakMap(value)) {
braces[0] = `${prefix}{`;
if (ctx.showHidden) {
formatter = formatWeakMap;
} else {
extra = '[items unknown]';
}
} else {
// Check boxed primitives other than string with valueOf()
// NOTE: `Date` has to be checked first!
Expand Down Expand Up @@ -616,6 +639,9 @@ function formatValue(ctx, value, recurseTimes, ln) {
ctx.seen.push(value);
const output = formatter(ctx, value, recurseTimes, keys);

if (extra !== undefined)
output.unshift(extra);

for (var i = 0; i < symbols.length; i++) {
output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0));
}
Expand Down Expand Up @@ -839,6 +865,46 @@ function formatMap(ctx, value, recurseTimes, keys) {
return output;
}

function formatWeakSet(ctx, value, recurseTimes, keys) {
const maxArrayLength = Math.max(ctx.maxArrayLength, 0);
const entries = previewWeakSet(value, maxArrayLength + 1);
const maxLength = Math.min(maxArrayLength, entries.length);
let output = new Array(maxLength);
for (var i = 0; i < maxLength; ++i)
output[i] = formatValue(ctx, entries[i], recurseTimes);
// Sort all entries to have a halfway reliable output (if more entries than
// retrieved ones exist, we can not reliably return the same output).
output = output.sort();
if (entries.length > maxArrayLength)
output.push('... more items');
for (i = 0; i < keys.length; i++)
output.push(formatProperty(ctx, value, recurseTimes, keys[i], 0));
return output;
}

function formatWeakMap(ctx, value, recurseTimes, keys) {
const maxArrayLength = Math.max(ctx.maxArrayLength, 0);
const entries = previewWeakMap(value, maxArrayLength + 1);
// Entries exist as [key1, val1, key2, val2, ...]
const remainder = entries.length / 2 > maxArrayLength;
const len = entries.length / 2 - (remainder ? 1 : 0);
const maxLength = Math.min(maxArrayLength, len);
let output = new Array(maxLength);
for (var i = 0; i < len; i++) {
const pos = i * 2;
output[i] = `${formatValue(ctx, entries[pos], recurseTimes)} => ` +
formatValue(ctx, entries[pos + 1], recurseTimes);
}
// Sort all entries to have a halfway reliable output (if more entries than
// retrieved ones exist, we can not reliably return the same output).
output = output.sort();
if (remainder > 0)
output.push('... more items');
for (i = 0; i < keys.length; i++)
output.push(formatProperty(ctx, value, recurseTimes, keys[i], 0));
return output;
}

function formatCollectionIterator(preview, ctx, value, recurseTimes, keys) {
const output = [];
for (const entry of preview(value)) {
Expand Down
48 changes: 48 additions & 0 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -1351,3 +1351,51 @@ util.inspect(process);
expect = '{\n a: \'12 45 78 01 34 \' +\n \'67 90 23\'\n}';
assert.strictEqual(out, expect);
}

{ // Test WeakMap
const obj = {};
const arr = [];
const weakMap = new WeakMap([[obj, arr], [arr, obj]]);
let out = util.inspect(weakMap, { showHidden: true });
let expect = 'WeakMap { [ [length]: 0 ] => {}, {} => [ [length]: 0 ] }';
assert.strictEqual(out, expect);

out = util.inspect(weakMap);
expect = 'WeakMap { [items unknown] }';
assert.strictEqual(out, expect);

out = util.inspect(weakMap, { maxArrayLength: 0, showHidden: true });
expect = 'WeakMap { ... more items }';
assert.strictEqual(out, expect);

weakMap.extra = true;
out = util.inspect(weakMap, { maxArrayLength: 1, showHidden: true });
// It is not possible to determine the output reliable.
expect = 'WeakMap { [ [length]: 0 ] => {}, ... more items, extra: true }';
const expectAlt = 'WeakMap { {} => [ [length]: 0 ], ... more items, ' +
'extra: true }';
assert(out === expect || out === expectAlt);
}

{ // Test WeakSet
const weakSet = new WeakSet([{}, [1]]);
let out = util.inspect(weakSet, { showHidden: true });
let expect = 'WeakSet { [ 1, [length]: 1 ], {} }';
assert.strictEqual(out, expect);

out = util.inspect(weakSet);
expect = 'WeakSet { [items unknown] }';
assert.strictEqual(out, expect);

out = util.inspect(weakSet, { maxArrayLength: -2, showHidden: true });
expect = 'WeakSet { ... more items }';
assert.strictEqual(out, expect);

weakSet.extra = true;
out = util.inspect(weakSet, { maxArrayLength: 1, showHidden: true });
// It is not possible to determine the output reliable.
expect = 'WeakSet { {}, ... more items, extra: true }';
const expectAlt = 'WeakSet { [ 1, [length]: 1 ], ... more items, ' +
'extra: true }';
assert(out === expect || out === expectAlt);
}

0 comments on commit 1029dd3

Please sign in to comment.