From 5a13e66088edf95551df5f17bc703790afdf7e1c Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sun, 12 Aug 2018 21:45:36 +0200 Subject: [PATCH] util: improve inspect performance This significantly improves the inspection performance for all array types. From now on only the visible elements cause work instead of having to process all array keys no matter how many entries are visible. This also moves some code out of the main function to reduce the overall function complexity. Backport-PR-URL: https://github.com/nodejs/node/pull/23039 PR-URL: https://github.com/nodejs/node/pull/22503 Reviewed-By: Matteo Collina Reviewed-By: John-David Dalton Reviewed-By: Benjamin Gruenbaum Reviewed-By: Refael Ackermann --- lib/util.js | 461 ++++++++++++++--------------- test/parallel/test-util-inspect.js | 2 +- 2 files changed, 222 insertions(+), 241 deletions(-) diff --git a/lib/util.js b/lib/util.js index 09e60a76366b39..ec9651ec97c495 100644 --- a/lib/util.js +++ b/lib/util.js @@ -32,11 +32,16 @@ const { TextDecoder, TextEncoder } = require('internal/encoding'); const { isBuffer } = require('buffer').Buffer; const { + getOwnNonIndexProperties, getPromiseDetails, getProxyDetails, kPending, kRejected, - previewEntries + previewEntries, + propertyFilter: { + ALL_PROPERTIES, + ONLY_ENUMERABLE + } } = process.binding('util'); const { internalBinding } = require('internal/bootstrap/loaders'); @@ -46,6 +51,7 @@ const { isAnyArrayBuffer, isArrayBuffer, isArgumentsObject, + isBoxedPrimitive, isDataView, isExternal, isMap, @@ -61,7 +67,6 @@ const { isStringObject, isNumberObject, isBooleanObject, - isSymbolObject, isBigIntObject, isUint8Array, isUint8ClampedArray, @@ -97,6 +102,10 @@ const inspectDefaultOptions = Object.seal({ compact: true }); +const kObjectType = 0; +const kArrayType = 1; +const kArrayExtrasType = 2; + const ReflectApply = Reflect.apply; // This function is borrowed from the function with the same name on V8 Extras' @@ -122,6 +131,7 @@ const stringValueOf = uncurryThis(String.prototype.valueOf); const setValues = uncurryThis(Set.prototype.values); const mapEntries = uncurryThis(Map.prototype.entries); const dateGetTime = uncurryThis(Date.prototype.getTime); +const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty); let CIRCULAR_ERROR_MESSAGE; let internalDeepEqual; @@ -425,10 +435,15 @@ function stylizeWithColor(str, styleType) { return str; } -function stylizeNoColor(str, styleType) { +function stylizeNoColor(str) { return str; } +// Return a new empty array to push in the results of the default formatter. +function getEmptyFormatArray() { + return []; +} + function getConstructorName(obj) { while (obj) { const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); @@ -461,6 +476,56 @@ function getPrefix(constructor, tag, fallback) { return ''; } +const getBoxedValue = formatPrimitive.bind(null, stylizeNoColor); + +// Look up the keys of the object. +function getKeys(value, showHidden) { + let keys; + const symbols = Object.getOwnPropertySymbols(value); + if (showHidden) { + keys = Object.getOwnPropertyNames(value); + if (symbols.length !== 0) + keys.push(...symbols); + } else { + // This might throw if `value` is a Module Namespace Object from an + // unevaluated module, but we don't want to perform the actual type + // check because it's expensive. + // TODO(devsnek): track https://github.com/tc39/ecma262/issues/1209 + // and modify this logic as needed. + try { + keys = Object.keys(value); + } catch (err) { + if (types.isNativeError(err) && + err.name === 'ReferenceError' && + types.isModuleNamespaceObject(value)) { + keys = Object.getOwnPropertyNames(value); + } else { + throw err; + } + } + if (symbols.length !== 0) { + keys.push(...symbols.filter((key) => propertyIsEnumerable(value, key))); + } + } + return keys; +} + +function formatProxy(ctx, proxy, recurseTimes) { + if (recurseTimes != null) { + if (recurseTimes < 0) + return ctx.stylize('Proxy [Array]', 'special'); + recurseTimes -= 1; + } + ctx.indentationLvl += 2; + const res = [ + formatValue(ctx, proxy[0], recurseTimes), + formatValue(ctx, proxy[1], recurseTimes) + ]; + ctx.indentationLvl -= 2; + const str = reduceToSingleString(ctx, res, '', ['[', ']']); + return `Proxy ${str}`; +} + function findTypedConstructor(value) { for (const [check, clazz] of [ [isUint8Array, Uint8Array], @@ -481,8 +546,6 @@ function findTypedConstructor(value) { } } -const getBoxedValue = formatPrimitive.bind(null, stylizeNoColor); - function noPrototypeIterator(ctx, value, recurseTimes) { let newVal; // TODO: Create a Subclass in case there's no prototype and show @@ -521,19 +584,7 @@ function formatValue(ctx, value, recurseTimes) { if (ctx.showProxy) { const proxy = getProxyDetails(value); if (proxy !== undefined) { - if (recurseTimes != null) { - if (recurseTimes < 0) - return ctx.stylize('Proxy [Array]', 'special'); - recurseTimes -= 1; - } - ctx.indentationLvl += 2; - const res = [ - formatValue(ctx, proxy[0], recurseTimes), - formatValue(ctx, proxy[1], recurseTimes) - ]; - ctx.indentationLvl -= 2; - const str = reduceToSingleString(ctx, res, '', ['[', ']']); - return `Proxy ${str}`; + return formatProxy(ctx, proxy, recurseTimes); } } @@ -574,78 +625,65 @@ function formatValue(ctx, value, recurseTimes) { if (ctx.seen.indexOf(value) !== -1) return ctx.stylize('[Circular]', 'special'); - let keys; - let symbols = Object.getOwnPropertySymbols(value); - - // Look up the keys of the object. - if (ctx.showHidden) { - keys = Object.getOwnPropertyNames(value); - } else { - // This might throw if `value` is a Module Namespace Object from an - // unevaluated module, but we don't want to perform the actual type - // check because it's expensive. - // TODO(devsnek): track https://github.com/tc39/ecma262/issues/1209 - // and modify this logic as needed. - try { - keys = Object.keys(value); - } catch (err) { - if (types.isNativeError(err) && - err.name === 'ReferenceError' && - types.isModuleNamespaceObject(value)) { - keys = Object.getOwnPropertyNames(value); - } else { - throw err; - } - } - - if (symbols.length !== 0) - symbols = symbols.filter((key) => propertyIsEnumerable(value, key)); - } + return formatRaw(ctx, value, recurseTimes); +} - const keyLength = keys.length + symbols.length; +function formatRaw(ctx, value, recurseTimes) { + let keys; const constructor = getConstructorName(value); let tag = value[Symbol.toStringTag]; if (typeof tag !== 'string') tag = ''; let base = ''; - let formatter = formatObject; + let formatter = getEmptyFormatArray; let braces; let noIterator = true; - let extra; let i = 0; + let skip = false; + const filter = ctx.showHidden ? ALL_PROPERTIES : ONLY_ENUMERABLE; + + let extrasType = kObjectType; // Iterators and the rest are split to reduce checks if (value[Symbol.iterator]) { noIterator = false; if (Array.isArray(value)) { + keys = getOwnNonIndexProperties(value, filter); // Only set the constructor for non ordinary ("Array [...]") arrays. const prefix = getPrefix(constructor, tag); braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']']; - if (value.length === 0 && keyLength === 0) + if (value.length === 0 && keys.length === 0) return `${braces[0]}]`; + extrasType = kArrayExtrasType; formatter = formatArray; } else if (isSet(value)) { + keys = getKeys(value, ctx.showHidden); const prefix = getPrefix(constructor, tag); - if (value.size === 0 && keyLength === 0) + if (value.size === 0 && keys.length === 0) return `${prefix}{}`; braces = [`${prefix}{`, '}']; formatter = formatSet; } else if (isMap(value)) { + keys = getKeys(value, ctx.showHidden); const prefix = getPrefix(constructor, tag); - if (value.size === 0 && keyLength === 0) + if (value.size === 0 && keys.length === 0) return `${prefix}{}`; braces = [`${prefix}{`, '}']; formatter = formatMap; } else if (isTypedArray(value)) { + keys = getOwnNonIndexProperties(value, filter); braces = [`${getPrefix(constructor, tag)}[`, ']']; - if (value.length === 0 && keyLength === 0 && !ctx.showHidden) + if (value.length === 0 && keys.length === 0 && !ctx.showHidden) return `${braces[0]}]`; formatter = formatTypedArray; + extrasType = kArrayExtrasType; } else if (isMapIterator(value)) { + keys = getKeys(value, ctx.showHidden); braces = [`[${tag}] {`, '}']; formatter = formatMapIterator; } else if (isSetIterator(value)) { + keys = getKeys(value, ctx.showHidden); braces = [`[${tag}] {`, '}']; formatter = formatSetIterator; } else { @@ -653,34 +691,35 @@ function formatValue(ctx, value, recurseTimes) { } } if (noIterator) { + keys = getKeys(value, ctx.showHidden); braces = ['{', '}']; if (constructor === 'Object') { if (isArgumentsObject(value)) { - if (keyLength === 0) + if (keys.length === 0) return '[Arguments] {}'; braces[0] = '[Arguments] {'; } else if (tag !== '') { braces[0] = `${getPrefix(constructor, tag)}{`; - if (keyLength === 0) { + if (keys.length === 0) { return `${braces[0]}}`; } - } else if (keyLength === 0) { + } else if (keys.length === 0) { return '{}'; } } else if (typeof value === 'function') { const type = constructor || tag || 'Function'; const name = `${type}${value.name ? `: ${value.name}` : ''}`; - if (keyLength === 0) + if (keys.length === 0) return ctx.stylize(`[${name}]`, 'special'); base = `[${name}]`; } else if (isRegExp(value)) { // Make RegExps say that they are RegExps - if (keyLength === 0 || recurseTimes < 0) + if (keys.length === 0 || recurseTimes < 0) return ctx.stylize(regExpToString(value), 'regexp'); base = `${regExpToString(value)}`; } else if (isDate(value)) { // Make dates with properties first say the date - if (keyLength === 0) { + if (keys.length === 0) { if (Number.isNaN(dateGetTime(value))) return ctx.stylize(String(value), 'date'); return ctx.stylize(dateToISOString(value), 'date'); @@ -699,7 +738,7 @@ function formatValue(ctx, value, recurseTimes) { const indentation = ' '.repeat(ctx.indentationLvl); base = formatError(value).replace(/\n/g, `\n${indentation}`); } - if (keyLength === 0) + if (keys.length === 0) return base; if (ctx.compact === false && stackStart !== -1) { @@ -707,14 +746,14 @@ function formatValue(ctx, value, recurseTimes) { base = `[${base.slice(0, stackStart)}]`; } } else if (isAnyArrayBuffer(value)) { - // Fast path for ArrayBuffer and SharedArrayBuffer. - // Can't do the same for DataView because it has a non-primitive - // .buffer property that we need to recurse for. let prefix = getPrefix(constructor, tag); if (prefix === '') { prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer '; } - if (keyLength === 0) + // Fast path for ArrayBuffer and SharedArrayBuffer. + // Can't do the same for DataView because it has a non-primitive + // .buffer property that we need to recurse for. + if (keys.length === 0) return prefix + `{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; braces[0] = `${prefix}{`; @@ -728,50 +767,42 @@ function formatValue(ctx, value, recurseTimes) { formatter = formatPromise; } else if (isWeakSet(value)) { braces[0] = `${getPrefix(constructor, tag, 'WeakSet')}{`; - if (ctx.showHidden) { - formatter = formatWeakSet; - } else { - extra = ctx.stylize('[items unknown]', 'special'); - } + formatter = ctx.showHidden ? formatWeakSet : formatWeakCollection; } else if (isWeakMap(value)) { braces[0] = `${getPrefix(constructor, tag, 'WeakMap')}{`; - if (ctx.showHidden) { - formatter = formatWeakMap; - } else { - extra = ctx.stylize('[items unknown]', 'special'); - } + formatter = ctx.showHidden ? formatWeakMap : formatWeakCollection; } else if (types.isModuleNamespaceObject(value)) { braces[0] = `[${tag}] {`; formatter = formatNamespaceObject; - } else if (isNumberObject(value)) { - base = `[Number: ${getBoxedValue(numberValueOf(value))}]`; - if (keyLength === 0) - return ctx.stylize(base, 'number'); - } else if (isBooleanObject(value)) { - base = `[Boolean: ${getBoxedValue(booleanValueOf(value))}]`; - if (keyLength === 0) - return ctx.stylize(base, 'boolean'); - } else if (isBigIntObject(value)) { - base = `[BigInt: ${getBoxedValue(bigIntValueOf(value))}]`; - if (keyLength === 0) - return ctx.stylize(base, 'bigint'); - } else if (isSymbolObject(value)) { - base = `[Symbol: ${getBoxedValue(symbolValueOf(value))}]`; - if (keyLength === 0) - return ctx.stylize(base, 'symbol'); - } else if (isStringObject(value)) { - const raw = stringValueOf(value); - base = `[String: ${getBoxedValue(raw, ctx)}]`; - if (keyLength === raw.length) - return ctx.stylize(base, 'string'); - // For boxed Strings, we have to remove the 0-n indexed entries, - // since they just noisy up the output and are redundant - // Make boxed primitive Strings look like such - keys = keys.slice(value.length); - braces = ['{', '}']; - // The input prototype got manipulated. Special handle these. - // We have to rebuild the information so we are able to display everything. + skip = true; + } else if (isBoxedPrimitive(value)) { + let type; + if (isNumberObject(value)) { + base = `[Number: ${getBoxedValue(numberValueOf(value))}]`; + type = 'number'; + } else if (isStringObject(value)) { + base = `[String: ${getBoxedValue(stringValueOf(value), ctx)}]`; + type = 'string'; + // For boxed Strings, we have to remove the 0-n indexed entries, + // since they just noisy up the output and are redundant + // Make boxed primitive Strings look like such + keys = keys.slice(value.length); + } else if (isBooleanObject(value)) { + base = `[Boolean: ${getBoxedValue(booleanValueOf(value))}]`; + type = 'boolean'; + } else if (isBigIntObject(value)) { + base = `[BigInt: ${getBoxedValue(bigIntValueOf(value))}]`; + type = 'bigint'; + } else { + base = `[Symbol: ${getBoxedValue(symbolValueOf(value))}]`; + type = 'symbol'; + } + if (keys.length === 0) { + return ctx.stylize(base, type); + } } else { + // The input prototype got manipulated. Special handle these. We have to + // rebuild the information so we are able to display everything. const specialIterator = noPrototypeIterator(ctx, value, recurseTimes); if (specialIterator) { return specialIterator; @@ -783,7 +814,7 @@ function formatValue(ctx, value, recurseTimes) { braces = [`[${tag || 'Set Iterator'}] {`, '}']; formatter = formatSetIterator; // Handle other regular objects again. - } else if (keyLength === 0) { + } else if (keys.length === 0) { if (isExternal(value)) return ctx.stylize('[External]', 'special'); return `${getPrefix(constructor, tag)}{}`; @@ -801,36 +832,34 @@ function formatValue(ctx, value, recurseTimes) { ctx.seen.push(value); let output; - // This corresponds to a depth of at least 333 and likely 500. - if (ctx.indentationLvl < 1000) { + try { output = formatter(ctx, value, recurseTimes, keys); - } else { - try { - output = formatter(ctx, value, recurseTimes, keys); - } catch (err) { - if (errors.isStackOverflowError(err)) { - ctx.seen.pop(); - return ctx.stylize( - `[${constructor || tag || 'Object'}: Inspection interrupted ` + - 'prematurely. Maximum call stack size exceeded.]', - 'special' - ); + if (skip === false) { + for (i = 0; i < keys.length; i++) { + output.push( + formatProperty(ctx, value, recurseTimes, keys[i], extrasType)); } - throw err; } + } catch (err) { + return handleMaxCallStackSize(ctx, err, constructor, tag); } - if (extra !== undefined) - output.unshift(extra); - - for (i = 0; i < symbols.length; i++) { - output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0)); - } - ctx.seen.pop(); return reduceToSingleString(ctx, output, base, braces); } +function handleMaxCallStackSize(ctx, err, constructor, tag) { + if (errors.isStackOverflowError(err)) { + ctx.seen.pop(); + return ctx.stylize( + `[${constructor || tag || 'Object'}: Inspection interrupted ` + + 'prematurely. Maximum call stack size exceeded.]', + 'special' + ); + } + throw err; +} + function formatNumber(fn, value) { // Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0. if (Object.is(value, -0)) @@ -891,20 +920,13 @@ function formatError(value) { return value.stack || errorToString(value); } -function formatObject(ctx, value, recurseTimes, keys) { - const len = keys.length; - const output = new Array(len); - for (var i = 0; i < len; i++) - output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 0); - return output; -} - function formatNamespaceObject(ctx, value, recurseTimes, keys) { const len = keys.length; const output = new Array(len); for (var i = 0; i < len; i++) { try { - output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 0); + output[i] = formatProperty(ctx, value, recurseTimes, keys[i], + kObjectType); } catch (err) { if (!(types.isNativeError(err) && err.name === 'ReferenceError')) { throw err; @@ -913,7 +935,7 @@ function formatNamespaceObject(ctx, value, recurseTimes, keys) { // line breaks are always correct. Otherwise it is very difficult to keep // this aligned, even though this is a hacky way of dealing with this. const tmp = { [keys[i]]: '' }; - output[i] = formatProperty(ctx, tmp, recurseTimes, keys[i], 0); + output[i] = formatProperty(ctx, tmp, recurseTimes, keys[i], kObjectType); const pos = output[i].lastIndexOf(' '); // We have to find the last whitespace and have to replace that value as // it will be visualized as a regular string. @@ -925,91 +947,67 @@ function formatNamespaceObject(ctx, value, recurseTimes, keys) { } // The array is sparse and/or has extra keys -function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) { - const output = []; - const keyLen = keys.length; - let i = 0; - for (const key of keys) { - if (output.length === maxLength) - break; - const index = +key; +function formatSpecialArray(ctx, value, recurseTimes, maxLength, output, i) { + const keys = Object.keys(value); + let index = i; + for (; i < keys.length && output.length < maxLength; i++) { + const key = keys[i]; + const tmp = +key; // Arrays can only have up to 2^32 - 1 entries - if (index > 2 ** 32 - 2) + if (tmp > 2 ** 32 - 2) { break; - if (`${i}` !== key) { - if (!numberRegExp.test(key)) + } + if (`${index}` !== key) { + if (!numberRegExp.test(key)) { break; - const emptyItems = index - i; + } + const emptyItems = tmp - index; const ending = emptyItems > 1 ? 's' : ''; const message = `<${emptyItems} empty item${ending}>`; output.push(ctx.stylize(message, 'undefined')); - i = index; - if (output.length === maxLength) + index = tmp; + if (output.length === maxLength) { break; + } } - output.push(formatProperty(ctx, value, recurseTimes, key, 1)); - i++; - } - if (i < valLen && output.length !== maxLength) { - const len = valLen - i; - const ending = len > 1 ? 's' : ''; - const message = `<${len} empty item${ending}>`; - output.push(ctx.stylize(message, 'undefined')); - i = valLen; - if (keyLen === 0) - return output; - } - const remaining = valLen - i; - if (remaining > 0) { - output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); + output.push(formatProperty(ctx, value, recurseTimes, key, kArrayType)); + index++; } - if (ctx.showHidden && keys[keyLen - 1] === 'length') { - // No extra keys - output.push(formatProperty(ctx, value, recurseTimes, 'length', 2)); - } else if (valLen === 0 || - keyLen > valLen && keys[valLen - 1] === `${valLen - 1}`) { - // The array is not sparse - for (i = valLen; i < keyLen; i++) - output.push(formatProperty(ctx, value, recurseTimes, keys[i], 2)); - } else if (keys[keyLen - 1] !== `${valLen - 1}`) { - const extra = []; - // Only handle special keys - let key; - for (i = keys.length - 1; i >= 0; i--) { - key = keys[i]; - if (numberRegExp.test(key) && +key < 2 ** 32 - 1) - break; - extra.push(formatProperty(ctx, value, recurseTimes, key, 2)); + const remaining = value.length - index; + if (output.length !== maxLength) { + if (remaining > 0) { + const ending = remaining > 1 ? 's' : ''; + const message = `<${remaining} empty item${ending}>`; + output.push(ctx.stylize(message, 'undefined')); } - for (i = extra.length - 1; i >= 0; i--) - output.push(extra[i]); + } else if (remaining > 0) { + output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); } return output; } -function formatArray(ctx, value, recurseTimes, keys) { - const len = Math.min(Math.max(0, ctx.maxArrayLength), value.length); - const hidden = ctx.showHidden ? 1 : 0; +function formatArray(ctx, value, recurseTimes) { const valLen = value.length; - const keyLen = keys.length - hidden; - if (keyLen !== valLen || keys[keyLen - 1] !== `${valLen - 1}`) - return formatSpecialArray(ctx, value, recurseTimes, keys, len, valLen); + const len = Math.min(Math.max(0, ctx.maxArrayLength), valLen); const remaining = valLen - len; - const output = new Array(len + (remaining > 0 ? 1 : 0) + hidden); - for (var i = 0; i < len; i++) - output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 1); + const output = []; + for (var i = 0; i < len; i++) { + // Special handle sparse arrays. + if (!hasOwnProperty(value, i)) { + return formatSpecialArray(ctx, value, recurseTimes, len, output, i); + } + output.push(formatProperty(ctx, value, recurseTimes, i, kArrayType)); + } if (remaining > 0) - output[i++] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`; - if (ctx.showHidden === true) - output[i] = formatProperty(ctx, value, recurseTimes, 'length', 2); + output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); return output; } -function formatTypedArray(ctx, value, recurseTimes, keys) { +function formatTypedArray(ctx, value, recurseTimes) { const maxLength = Math.min(Math.max(0, ctx.maxArrayLength), value.length); const remaining = value.length - maxLength; - const output = new Array(maxLength + (remaining > 0 ? 1 : 0)); + const output = new Array(maxLength); for (var i = 0; i < maxLength; ++i) output[i] = formatNumber(ctx.stylize, value[i]); if (remaining > 0) @@ -1029,52 +1027,39 @@ function formatTypedArray(ctx, value, recurseTimes, keys) { } ctx.indentationLvl -= 2; } - // TypedArrays cannot have holes. Therefore it is safe to assume that all - // extra keys are indexed after value.length. - for (i = value.length; i < keys.length; i++) { - output.push(formatProperty(ctx, value, recurseTimes, keys[i], 2)); - } return output; } -function formatSet(ctx, value, recurseTimes, keys) { - const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); - let i = 0; +function formatSet(ctx, value, recurseTimes) { + const output = []; ctx.indentationLvl += 2; for (const v of value) { - output[i++] = formatValue(ctx, v, recurseTimes); + output.push(formatValue(ctx, v, recurseTimes)); } ctx.indentationLvl -= 2; // With `showHidden`, `length` will display as a hidden property for // arrays. For consistency's sake, do the same for `size`, even though this // property isn't selected by Object.getOwnPropertyNames(). if (ctx.showHidden) - output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; - for (var n = 0; n < keys.length; n++) { - output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); - } + output.push(`[size]: ${ctx.stylize(`${value.size}`, 'number')}`); return output; } -function formatMap(ctx, value, recurseTimes, keys) { - const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); - let i = 0; +function formatMap(ctx, value, recurseTimes) { + const output = []; ctx.indentationLvl += 2; for (const [k, v] of value) { - output[i++] = `${formatValue(ctx, k, recurseTimes)} => ` + - formatValue(ctx, v, recurseTimes); + output.push(`${formatValue(ctx, k, recurseTimes)} => ` + + formatValue(ctx, v, recurseTimes)); } ctx.indentationLvl -= 2; // See comment in formatSet if (ctx.showHidden) - output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; - for (var n = 0; n < keys.length; n++) { - output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); - } + output.push(`[size]: ${ctx.stylize(`${value.size}`, 'number')}`); return output; } -function formatSetIterInner(ctx, value, recurseTimes, keys, entries, state) { +function formatSetIterInner(ctx, recurseTimes, entries, state) { const maxArrayLength = Math.max(ctx.maxArrayLength, 0); const maxLength = Math.min(maxArrayLength, entries.length); let output = new Array(maxLength); @@ -1092,12 +1077,10 @@ function formatSetIterInner(ctx, value, recurseTimes, keys, entries, state) { if (remaining > 0) { output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); } - for (i = 0; i < keys.length; i++) - output.push(formatProperty(ctx, value, recurseTimes, keys[i], 0)); return output; } -function formatMapIterInner(ctx, value, recurseTimes, keys, entries, state) { +function formatMapIterInner(ctx, recurseTimes, entries, state) { const maxArrayLength = Math.max(ctx.maxArrayLength, 0); // Entries exist as [key1, val1, key2, val2, ...] const len = entries.length / 2; @@ -1128,37 +1111,38 @@ function formatMapIterInner(ctx, value, recurseTimes, keys, entries, state) { if (remaining > 0) { output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); } - for (i = 0; i < keys.length; i++) - output.push(formatProperty(ctx, value, recurseTimes, keys[i], 0)); return output; } -function formatWeakSet(ctx, value, recurseTimes, keys) { +function formatWeakCollection(ctx) { + return [ctx.stylize('[items unknown]', 'special')]; +} + +function formatWeakSet(ctx, value, recurseTimes) { const entries = previewEntries(value); - return formatSetIterInner(ctx, value, recurseTimes, keys, entries, kWeak); + return formatSetIterInner(ctx, recurseTimes, entries, kWeak); } -function formatWeakMap(ctx, value, recurseTimes, keys) { +function formatWeakMap(ctx, value, recurseTimes) { const entries = previewEntries(value); - return formatMapIterInner(ctx, value, recurseTimes, keys, entries, kWeak); + return formatMapIterInner(ctx, recurseTimes, entries, kWeak); } -function formatSetIterator(ctx, value, recurseTimes, keys) { +function formatSetIterator(ctx, value, recurseTimes) { const entries = previewEntries(value); - return formatSetIterInner(ctx, value, recurseTimes, keys, entries, kIterator); + return formatSetIterInner(ctx, recurseTimes, entries, kIterator); } -function formatMapIterator(ctx, value, recurseTimes, keys) { +function formatMapIterator(ctx, value, recurseTimes) { const [entries, isKeyValue] = previewEntries(value, true); if (isKeyValue) { - return formatMapIterInner( - ctx, value, recurseTimes, keys, entries, kMapEntries); + return formatMapIterInner(ctx, recurseTimes, entries, kMapEntries); } - return formatSetIterInner(ctx, value, recurseTimes, keys, entries, kIterator); + return formatSetIterInner(ctx, recurseTimes, entries, kIterator); } -function formatPromise(ctx, value, recurseTimes, keys) { +function formatPromise(ctx, value, recurseTimes) { let output; const [state, result] = getPromiseDetails(value); if (state === kPending) { @@ -1175,19 +1159,16 @@ function formatPromise(ctx, value, recurseTimes, keys) { str ]; } - for (var n = 0; n < keys.length; n++) { - output.push(formatProperty(ctx, value, recurseTimes, keys[n], 0)); - } return output; } -function formatProperty(ctx, value, recurseTimes, key, array) { +function formatProperty(ctx, value, recurseTimes, key, type) { let name, str; let extra = ' '; const desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key], enumerable: true }; if (desc.value !== undefined) { - const diff = array !== 0 || ctx.compact === false ? 2 : 3; + const diff = (type !== kObjectType || ctx.compact === false) ? 2 : 3; ctx.indentationLvl += diff; str = formatValue(ctx, desc.value, recurseTimes); if (diff === 3) { @@ -1208,7 +1189,7 @@ function formatProperty(ctx, value, recurseTimes, key, array) { } else { str = ctx.stylize('undefined', 'undefined'); } - if (array === 1) { + if (type === kArrayType) { return str; } if (typeof key === 'symbol') { diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index df1348e95abf4d..e7585a86146b6b 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -922,7 +922,7 @@ if (typeof Symbol !== 'undefined') { const set = new Set(['foo']); set.bar = 42; assert.strictEqual( - util.inspect(set, true), + util.inspect(set, { showHidden: true }), "Set { 'foo', [size]: 1, bar: 42 }" ); }