diff --git a/doc/api/util.md b/doc/api/util.md index 8a4e6a0960ed13..92d9ec1dcd9c71 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 4b5498a07da9cf..425519e91d1203 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -472,8 +472,10 @@ // functions lazily (unless --nolazy is set) so we need to do this // before we turn off --allow_natives_syntax again. const v8 = NativeModule.require('internal/v8'); - v8.previewMapIterator(new Map().entries(), 1); - v8.previewSetIterator(new Set().entries(), 1); + 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 b80e3de33d6155..3102b45a2d4032 100644 --- a/lib/internal/v8.js +++ b/lib/internal/v8.js @@ -1,21 +1,39 @@ 'use strict'; -function take(it, n) { - const result = []; - for (const e of it) { - if (--n < 0) - break; - result.push(e); - } - return result; +// This file provides access to some of V8's native runtime functions. See +// https://github.com/v8/v8/wiki/Built-in-functions for further information +// about their implementation. +// They have to be loaded before anything else to make sure we deactivate them +// before executing any other code. Gaining access is achieved by using a +// specific flag that is used internally in the startup phase. + +// Clone the provided Map Iterator. +function previewMapIterator(it) { + return %MapIteratorClone(it); +} + +// Clone the provided Set Iterator. +function previewSetIterator(it) { + return %SetIteratorClone(it); } -function previewMapIterator(it, n) { - return take(%MapIteratorClone(it), n); +// 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); } -function previewSetIterator(it, n) { - return take(%SetIteratorClone(it), n); +// 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 }; +module.exports = { + previewMapIterator, + previewSetIterator, + previewWeakMap, + previewWeakSet +}; diff --git a/lib/util.js b/lib/util.js index eabdf6c1df6d6d..4a5e33fc45b123 100644 --- a/lib/util.js +++ b/lib/util.js @@ -30,7 +30,12 @@ const { const { TextDecoder, TextEncoder } = require('internal/encoding'); const { isBuffer } = require('buffer').Buffer; -const { previewMapIterator, previewSetIterator } = require('internal/v8'); +const { + previewMapIterator, + previewSetIterator, + previewWeakMap, + previewWeakSet +} = require('internal/v8'); const { getPromiseDetails, @@ -51,6 +56,8 @@ const { isPromise, isSet, isSetIterator, + isWeakMap, + isWeakSet, isRegExp, isDate, isTypedArray @@ -288,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, @@ -325,6 +334,8 @@ Object.defineProperty(inspect, 'defaultOptions', { if (options === null || typeof options !== 'object') { throw new ERR_INVALID_ARG_TYPE('options', 'Object'); } + // TODO(BridgeAR): Add input validation and make sure `defaultOptions` are + // not configurable. return _extend(inspectDefaultOptions, options); } }); @@ -462,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]) { @@ -559,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! @@ -613,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)); } @@ -836,25 +865,69 @@ function formatMap(ctx, value, recurseTimes, keys) { return output; } -function formatCollectionIterator(preview, ctx, value, recurseTimes, - visibleKeys, keys) { - const nextRecurseTimes = recurseTimes === null ? null : recurseTimes - 1; - const vals = preview(value, 100); +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 o of vals) { - output.push(formatValue(ctx, o, nextRecurseTimes)); + for (const entry of preview(value)) { + if (ctx.maxArrayLength === output.length) { + output.push('... more items'); + break; + } + output.push(formatValue(ctx, entry, recurseTimes)); + } + for (var n = 0; n < keys.length; n++) { + output.push(formatProperty(ctx, value, recurseTimes, keys[n], 0)); } return output; } -function formatMapIterator(ctx, value, recurseTimes, visibleKeys, keys) { +function formatMapIterator(ctx, value, recurseTimes, keys) { return formatCollectionIterator(previewMapIterator, ctx, value, recurseTimes, - visibleKeys, keys); + keys); } -function formatSetIterator(ctx, value, recurseTimes, visibleKeys, keys) { +function formatSetIterator(ctx, value, recurseTimes, keys) { return formatCollectionIterator(previewSetIterator, ctx, value, recurseTimes, - visibleKeys, keys); + keys); } function formatPromise(ctx, value, recurseTimes, keys) { diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index b2c8207de54c83..6d0089fcb7fea8 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -448,7 +448,7 @@ assert.strictEqual(util.inspect(-5e-324), '-5e-324'); { const map = new Map(); map.set(1, 2); - const vals = previewMapIterator(map.entries(), 100); + const vals = previewMapIterator(map.entries()); const valsOutput = []; for (const o of vals) { valsOutput.push(o); @@ -924,6 +924,10 @@ if (typeof Symbol !== 'undefined') { const keys = map.keys(); assert.strictEqual(util.inspect(keys), '[Map Iterator] { \'foo\' }'); assert.strictEqual(util.inspect(keys), '[Map Iterator] { \'foo\' }'); + keys.extra = true; + assert.strictEqual( + util.inspect(keys, { maxArrayLength: 0 }), + '[Map Iterator] { ... more items, extra: true }'); } // Test Set iterators. @@ -937,6 +941,10 @@ if (typeof Symbol !== 'undefined') { const keys = aSet.keys(); assert.strictEqual(util.inspect(keys), '[Set Iterator] { 1, 3 }'); assert.strictEqual(util.inspect(keys), '[Set Iterator] { 1, 3 }'); + keys.extra = true; + assert.strictEqual( + util.inspect(keys, { maxArrayLength: 1 }), + '[Set Iterator] { 1, ... more items, extra: true }'); } // Test alignment of items in container. @@ -1341,3 +1349,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); +}