Skip to content

Commit 1029dd3

Browse files
committed
util: show Weak(Set|Map) entries in inspect
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>
1 parent 0fbd4b1 commit 1029dd3

File tree

5 files changed

+166
-7
lines changed

5 files changed

+166
-7
lines changed

doc/api/util.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,9 @@ stream.write('With ES6');
345345
<!-- YAML
346346
added: v0.3.0
347347
changes:
348+
- version: REPLACEME
349+
pr-url: https://github.com/nodejs/node/pull/19259
350+
description: WeakMap and WeakSet entries can now be inspected as well.
348351
- version: REPLACEME
349352
pr-url: https://github.com/nodejs/node/pull/17907
350353
description: The `depth` default changed to Infinity.
@@ -369,7 +372,8 @@ changes:
369372
* `object` {any} Any JavaScript primitive or Object.
370373
* `options` {Object}
371374
* `showHidden` {boolean} If `true`, the `object`'s non-enumerable symbols and
372-
properties will be included in the formatted result. Defaults to `false`.
375+
properties will be included in the formatted result as well as [`WeakMap`][]
376+
and [`WeakSet`][] entries. Defaults to `false`.
373377
* `colors` {boolean} If `true`, the output will be styled with ANSI color
374378
codes. Defaults to `false`. Colors are customizable, see
375379
[Customizing `util.inspect` colors][].
@@ -378,10 +382,14 @@ changes:
378382
* `showProxy` {boolean} If `true`, then objects and functions that are
379383
`Proxy` objects will be introspected to show their `target` and `handler`
380384
objects. Defaults to `false`.
381-
* `maxArrayLength` {number} Specifies the maximum number of array and
382-
`TypedArray` elements to include when formatting. Defaults to `100`. Set to
383-
`null` or `Infinity` to show all array elements. Set to `0` or negative to
384-
show no array elements.
385+
<!--
386+
TODO(BridgeAR): Deprecate `maxArrayLength` and replace it with
387+
`maxEntries`.
388+
-->
389+
* `maxArrayLength` {number} Specifies the maximum number of `Array`,
390+
[`TypedArray`][], [`WeakMap`][] and [`WeakSet`][] elements to include when
391+
formatting. Defaults to `100`. Set to `null` or `Infinity` to show all
392+
elements. Set to `0` or negative to show no elements.
385393
* `breakLength` {number} The length at which an object's keys are split
386394
across multiple lines. Set to `Infinity` to format an object as a single
387395
line. Defaults to 60 for legacy compatibility.
@@ -501,6 +509,25 @@ console.log(util.inspect(o, { compact: false, breakLength: 80 }));
501509
// chunks.
502510
```
503511

512+
Using the `showHidden` option allows to inspect [`WeakMap`][] and [`WeakSet`][]
513+
entries. If there are more entries than `maxArrayLength`, there is no guarantee
514+
which entries are displayed. That means retrieving the same ['WeakSet'][]
515+
entries twice might actually result in a different output. Besides this any item
516+
might be collected at any point of time by the garbage collector if there is no
517+
strong reference left to that object. Therefore there is no guarantee to get a
518+
reliable output.
519+
520+
```js
521+
const { inspect } = require('util');
522+
523+
const obj = { a: 1 };
524+
const obj2 = { b: 2 };
525+
const weakSet = new WeakSet([obj, obj2]);
526+
527+
console.log(inspect(weakSet, { showHidden: true }));
528+
// WeakSet { { a: 1 }, { b: 2 } }
529+
```
530+
504531
Please note that `util.inspect()` is a synchronous method that is mainly
505532
intended as a debugging tool. Some input values can have a significant
506533
performance overhead that can block the event loop. Use this function

lib/internal/bootstrap/node.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,8 @@
473473
const v8 = NativeModule.require('internal/v8');
474474
v8.previewMapIterator(new Map().entries());
475475
v8.previewSetIterator(new Set().entries());
476+
v8.previewWeakMap(new WeakMap(), 1);
477+
v8.previewWeakSet(new WeakSet(), 1);
476478
// Disable --allow_natives_syntax again unless it was explicitly
477479
// specified on the command line.
478480
const re = /^--allow[-_]natives[-_]syntax$/;

lib/internal/v8.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,23 @@ function previewSetIterator(it) {
1717
return %SetIteratorClone(it);
1818
}
1919

20+
// Retrieve all WeakMap instance key / value pairs up to `max`. `max` limits the
21+
// number of key / value pairs returned. Make sure it is a positive number,
22+
// otherwise V8 aborts. Passing through `0` returns all elements.
23+
function previewWeakMap(weakMap, max) {
24+
return %GetWeakMapEntries(weakMap, max);
25+
}
26+
27+
// Retrieve all WeakSet instance values up to `max`. `max` limits the
28+
// number of key / value pairs returned. Make sure it is a positive number,
29+
// otherwise V8 aborts. Passing through `0` returns all elements.
30+
function previewWeakSet(weakSet, max) {
31+
return %GetWeakSetValues(weakSet, max);
32+
}
33+
2034
module.exports = {
2135
previewMapIterator,
22-
previewSetIterator
36+
previewSetIterator,
37+
previewWeakMap,
38+
previewWeakSet
2339
};

lib/util.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ const { isBuffer } = require('buffer').Buffer;
3232

3333
const {
3434
previewMapIterator,
35-
previewSetIterator
35+
previewSetIterator,
36+
previewWeakMap,
37+
previewWeakSet
3638
} = require('internal/v8');
3739

3840
const {
@@ -54,6 +56,8 @@ const {
5456
isPromise,
5557
isSet,
5658
isSetIterator,
59+
isWeakMap,
60+
isWeakSet,
5761
isRegExp,
5862
isDate,
5963
isTypedArray
@@ -291,6 +295,8 @@ function inspect(value, opts) {
291295
colors: inspectDefaultOptions.colors,
292296
customInspect: inspectDefaultOptions.customInspect,
293297
showProxy: inspectDefaultOptions.showProxy,
298+
// TODO(BridgeAR): Deprecate `maxArrayLength` and replace it with
299+
// `maxEntries`.
294300
maxArrayLength: inspectDefaultOptions.maxArrayLength,
295301
breakLength: inspectDefaultOptions.breakLength,
296302
indentationLvl: 0,
@@ -328,6 +334,8 @@ Object.defineProperty(inspect, 'defaultOptions', {
328334
if (options === null || typeof options !== 'object') {
329335
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
330336
}
337+
// TODO(BridgeAR): Add input validation and make sure `defaultOptions` are
338+
// not configurable.
331339
return _extend(inspectDefaultOptions, options);
332340
}
333341
});
@@ -465,6 +473,7 @@ function formatValue(ctx, value, recurseTimes, ln) {
465473
let braces;
466474
let noIterator = true;
467475
let raw;
476+
let extra;
468477

469478
// Iterators and the rest are split to reduce checks
470479
if (value[Symbol.iterator]) {
@@ -562,6 +571,20 @@ function formatValue(ctx, value, recurseTimes, ln) {
562571
} else if (isPromise(value)) {
563572
braces[0] = `${prefix}{`;
564573
formatter = formatPromise;
574+
} else if (isWeakSet(value)) {
575+
braces[0] = `${prefix}{`;
576+
if (ctx.showHidden) {
577+
formatter = formatWeakSet;
578+
} else {
579+
extra = '[items unknown]';
580+
}
581+
} else if (isWeakMap(value)) {
582+
braces[0] = `${prefix}{`;
583+
if (ctx.showHidden) {
584+
formatter = formatWeakMap;
585+
} else {
586+
extra = '[items unknown]';
587+
}
565588
} else {
566589
// Check boxed primitives other than string with valueOf()
567590
// NOTE: `Date` has to be checked first!
@@ -616,6 +639,9 @@ function formatValue(ctx, value, recurseTimes, ln) {
616639
ctx.seen.push(value);
617640
const output = formatter(ctx, value, recurseTimes, keys);
618641

642+
if (extra !== undefined)
643+
output.unshift(extra);
644+
619645
for (var i = 0; i < symbols.length; i++) {
620646
output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0));
621647
}
@@ -839,6 +865,46 @@ function formatMap(ctx, value, recurseTimes, keys) {
839865
return output;
840866
}
841867

868+
function formatWeakSet(ctx, value, recurseTimes, keys) {
869+
const maxArrayLength = Math.max(ctx.maxArrayLength, 0);
870+
const entries = previewWeakSet(value, maxArrayLength + 1);
871+
const maxLength = Math.min(maxArrayLength, entries.length);
872+
let output = new Array(maxLength);
873+
for (var i = 0; i < maxLength; ++i)
874+
output[i] = formatValue(ctx, entries[i], recurseTimes);
875+
// Sort all entries to have a halfway reliable output (if more entries than
876+
// retrieved ones exist, we can not reliably return the same output).
877+
output = output.sort();
878+
if (entries.length > maxArrayLength)
879+
output.push('... more items');
880+
for (i = 0; i < keys.length; i++)
881+
output.push(formatProperty(ctx, value, recurseTimes, keys[i], 0));
882+
return output;
883+
}
884+
885+
function formatWeakMap(ctx, value, recurseTimes, keys) {
886+
const maxArrayLength = Math.max(ctx.maxArrayLength, 0);
887+
const entries = previewWeakMap(value, maxArrayLength + 1);
888+
// Entries exist as [key1, val1, key2, val2, ...]
889+
const remainder = entries.length / 2 > maxArrayLength;
890+
const len = entries.length / 2 - (remainder ? 1 : 0);
891+
const maxLength = Math.min(maxArrayLength, len);
892+
let output = new Array(maxLength);
893+
for (var i = 0; i < len; i++) {
894+
const pos = i * 2;
895+
output[i] = `${formatValue(ctx, entries[pos], recurseTimes)} => ` +
896+
formatValue(ctx, entries[pos + 1], recurseTimes);
897+
}
898+
// Sort all entries to have a halfway reliable output (if more entries than
899+
// retrieved ones exist, we can not reliably return the same output).
900+
output = output.sort();
901+
if (remainder > 0)
902+
output.push('... more items');
903+
for (i = 0; i < keys.length; i++)
904+
output.push(formatProperty(ctx, value, recurseTimes, keys[i], 0));
905+
return output;
906+
}
907+
842908
function formatCollectionIterator(preview, ctx, value, recurseTimes, keys) {
843909
const output = [];
844910
for (const entry of preview(value)) {

test/parallel/test-util-inspect.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,3 +1351,51 @@ util.inspect(process);
13511351
expect = '{\n a: \'12 45 78 01 34 \' +\n \'67 90 23\'\n}';
13521352
assert.strictEqual(out, expect);
13531353
}
1354+
1355+
{ // Test WeakMap
1356+
const obj = {};
1357+
const arr = [];
1358+
const weakMap = new WeakMap([[obj, arr], [arr, obj]]);
1359+
let out = util.inspect(weakMap, { showHidden: true });
1360+
let expect = 'WeakMap { [ [length]: 0 ] => {}, {} => [ [length]: 0 ] }';
1361+
assert.strictEqual(out, expect);
1362+
1363+
out = util.inspect(weakMap);
1364+
expect = 'WeakMap { [items unknown] }';
1365+
assert.strictEqual(out, expect);
1366+
1367+
out = util.inspect(weakMap, { maxArrayLength: 0, showHidden: true });
1368+
expect = 'WeakMap { ... more items }';
1369+
assert.strictEqual(out, expect);
1370+
1371+
weakMap.extra = true;
1372+
out = util.inspect(weakMap, { maxArrayLength: 1, showHidden: true });
1373+
// It is not possible to determine the output reliable.
1374+
expect = 'WeakMap { [ [length]: 0 ] => {}, ... more items, extra: true }';
1375+
const expectAlt = 'WeakMap { {} => [ [length]: 0 ], ... more items, ' +
1376+
'extra: true }';
1377+
assert(out === expect || out === expectAlt);
1378+
}
1379+
1380+
{ // Test WeakSet
1381+
const weakSet = new WeakSet([{}, [1]]);
1382+
let out = util.inspect(weakSet, { showHidden: true });
1383+
let expect = 'WeakSet { [ 1, [length]: 1 ], {} }';
1384+
assert.strictEqual(out, expect);
1385+
1386+
out = util.inspect(weakSet);
1387+
expect = 'WeakSet { [items unknown] }';
1388+
assert.strictEqual(out, expect);
1389+
1390+
out = util.inspect(weakSet, { maxArrayLength: -2, showHidden: true });
1391+
expect = 'WeakSet { ... more items }';
1392+
assert.strictEqual(out, expect);
1393+
1394+
weakSet.extra = true;
1395+
out = util.inspect(weakSet, { maxArrayLength: 1, showHidden: true });
1396+
// It is not possible to determine the output reliable.
1397+
expect = 'WeakSet { {}, ... more items, extra: true }';
1398+
const expectAlt = 'WeakSet { [ 1, [length]: 1 ], ... more items, ' +
1399+
'extra: true }';
1400+
assert(out === expect || out === expectAlt);
1401+
}

0 commit comments

Comments
 (0)