From 1029dd36861d7ab592d4e219362706d2c161839a Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Fri, 9 Mar 2018 15:03:44 +0100 Subject: [PATCH] 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: https://github.com/nodejs/node/pull/19259 Fixes: https://github.com/nodejs/node/issues/19001: Reviewed-By: Yosuke Furukawa Reviewed-By: Matteo Collina Reviewed-By: Tiancheng "Timothy" Gu Reviewed-By: James M Snell --- doc/api/util.md | 37 +++++++++++++--- lib/internal/bootstrap/node.js | 2 + lib/internal/v8.js | 18 +++++++- lib/util.js | 68 +++++++++++++++++++++++++++++- test/parallel/test-util-inspect.js | 48 +++++++++++++++++++++ 5 files changed, 166 insertions(+), 7 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 8a4e6a0960ed13..9253d46351d3f4 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -345,6 +345,9 @@ stream.write('With ES6'); + * `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. @@ -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 diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 9055e8c41986c3..01efe6f32900d4 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -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$/; diff --git a/lib/internal/v8.js b/lib/internal/v8.js index 92d5beb7c4ca37..3102b45a2d4032 100644 --- a/lib/internal/v8.js +++ b/lib/internal/v8.js @@ -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 }; diff --git a/lib/util.js b/lib/util.js index 774b185095c53a..a9be7cbda4f2a0 100644 --- a/lib/util.js +++ b/lib/util.js @@ -32,7 +32,9 @@ const { isBuffer } = require('buffer').Buffer; const { previewMapIterator, - previewSetIterator + previewSetIterator, + previewWeakMap, + previewWeakSet } = require('internal/v8'); const { @@ -54,6 +56,8 @@ const { isPromise, isSet, isSetIterator, + isWeakMap, + isWeakSet, isRegExp, isDate, isTypedArray @@ -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, @@ -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); } }); @@ -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]) { @@ -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! @@ -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)); } @@ -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)) { diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index ab27e69c183202..bdf446412126c0 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -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); +}