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.

PR-URL: https://github.com/nodejs/node/pull/22503
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: John-David Dalton <john.david.dalton@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
This commit is contained in:
Ruben Bridgewater 2018-08-12 21:45:36 +02:00
parent de33a5aa6e
commit 12ed7c94e5
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
2 changed files with 223 additions and 242 deletions

View File

@ -33,11 +33,16 @@ const { isBuffer } = require('buffer').Buffer;
const { internalBinding } = require('internal/bootstrap/loaders'); const { internalBinding } = require('internal/bootstrap/loaders');
const { const {
getOwnNonIndexProperties,
getPromiseDetails, getPromiseDetails,
getProxyDetails, getProxyDetails,
kPending, kPending,
kRejected, kRejected,
previewEntries previewEntries,
propertyFilter: {
ALL_PROPERTIES,
ONLY_ENUMERABLE
}
} = internalBinding('util'); } = internalBinding('util');
const types = internalBinding('types'); const types = internalBinding('types');
@ -46,6 +51,7 @@ const {
isAnyArrayBuffer, isAnyArrayBuffer,
isArrayBuffer, isArrayBuffer,
isArgumentsObject, isArgumentsObject,
isBoxedPrimitive,
isDataView, isDataView,
isExternal, isExternal,
isMap, isMap,
@ -61,7 +67,6 @@ const {
isStringObject, isStringObject,
isNumberObject, isNumberObject,
isBooleanObject, isBooleanObject,
isSymbolObject,
isBigIntObject, isBigIntObject,
isUint8Array, isUint8Array,
isUint8ClampedArray, isUint8ClampedArray,
@ -97,6 +102,10 @@ const inspectDefaultOptions = Object.seal({
compact: true compact: true
}); });
const kObjectType = 0;
const kArrayType = 1;
const kArrayExtrasType = 2;
const ReflectApply = Reflect.apply; const ReflectApply = Reflect.apply;
// This function is borrowed from the function with the same name on V8 Extras' // 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 setValues = uncurryThis(Set.prototype.values);
const mapEntries = uncurryThis(Map.prototype.entries); const mapEntries = uncurryThis(Map.prototype.entries);
const dateGetTime = uncurryThis(Date.prototype.getTime); const dateGetTime = uncurryThis(Date.prototype.getTime);
const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty);
let CIRCULAR_ERROR_MESSAGE; let CIRCULAR_ERROR_MESSAGE;
let internalDeepEqual; let internalDeepEqual;
@ -475,10 +485,15 @@ function stylizeWithColor(str, styleType) {
return str; return str;
} }
function stylizeNoColor(str, styleType) { function stylizeNoColor(str) {
return str; return str;
} }
// Return a new empty array to push in the results of the default formatter.
function getEmptyFormatArray() {
return [];
}
function getConstructorName(obj) { function getConstructorName(obj) {
while (obj) { while (obj) {
const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor');
@ -511,6 +526,56 @@ function getPrefix(constructor, tag, fallback) {
return ''; 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) { function findTypedConstructor(value) {
for (const [check, clazz] of [ for (const [check, clazz] of [
[isUint8Array, Uint8Array], [isUint8Array, Uint8Array],
@ -531,8 +596,6 @@ function findTypedConstructor(value) {
} }
} }
const getBoxedValue = formatPrimitive.bind(null, stylizeNoColor);
function noPrototypeIterator(ctx, value, recurseTimes) { function noPrototypeIterator(ctx, value, recurseTimes) {
let newVal; let newVal;
// TODO: Create a Subclass in case there's no prototype and show // TODO: Create a Subclass in case there's no prototype and show
@ -571,19 +634,7 @@ function formatValue(ctx, value, recurseTimes) {
if (ctx.showProxy) { if (ctx.showProxy) {
const proxy = getProxyDetails(value); const proxy = getProxyDetails(value);
if (proxy !== undefined) { if (proxy !== undefined) {
if (recurseTimes != null) { return formatProxy(ctx, proxy, recurseTimes);
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}`;
} }
} }
@ -614,78 +665,65 @@ function formatValue(ctx, value, recurseTimes) {
if (ctx.seen.indexOf(value) !== -1) if (ctx.seen.indexOf(value) !== -1)
return ctx.stylize('[Circular]', 'special'); return ctx.stylize('[Circular]', 'special');
return formatRaw(ctx, value, recurseTimes);
}
function formatRaw(ctx, value, recurseTimes) {
let keys; 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));
}
const keyLength = keys.length + symbols.length;
const constructor = getConstructorName(value); const constructor = getConstructorName(value);
let tag = value[Symbol.toStringTag]; let tag = value[Symbol.toStringTag];
if (typeof tag !== 'string') if (typeof tag !== 'string')
tag = ''; tag = '';
let base = ''; let base = '';
let formatter = formatObject; let formatter = getEmptyFormatArray;
let braces; let braces;
let noIterator = true; let noIterator = true;
let extra;
let i = 0; 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 // Iterators and the rest are split to reduce checks
if (value[Symbol.iterator]) { if (value[Symbol.iterator]) {
noIterator = false; noIterator = false;
if (Array.isArray(value)) { if (Array.isArray(value)) {
keys = getOwnNonIndexProperties(value, filter);
// Only set the constructor for non ordinary ("Array [...]") arrays. // Only set the constructor for non ordinary ("Array [...]") arrays.
const prefix = getPrefix(constructor, tag); const prefix = getPrefix(constructor, tag);
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']']; braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
if (value.length === 0 && keyLength === 0) if (value.length === 0 && keys.length === 0)
return `${braces[0]}]`; return `${braces[0]}]`;
extrasType = kArrayExtrasType;
formatter = formatArray; formatter = formatArray;
} else if (isSet(value)) { } else if (isSet(value)) {
keys = getKeys(value, ctx.showHidden);
const prefix = getPrefix(constructor, tag); const prefix = getPrefix(constructor, tag);
if (value.size === 0 && keyLength === 0) if (value.size === 0 && keys.length === 0)
return `${prefix}{}`; return `${prefix}{}`;
braces = [`${prefix}{`, '}']; braces = [`${prefix}{`, '}'];
formatter = formatSet; formatter = formatSet;
} else if (isMap(value)) { } else if (isMap(value)) {
keys = getKeys(value, ctx.showHidden);
const prefix = getPrefix(constructor, tag); const prefix = getPrefix(constructor, tag);
if (value.size === 0 && keyLength === 0) if (value.size === 0 && keys.length === 0)
return `${prefix}{}`; return `${prefix}{}`;
braces = [`${prefix}{`, '}']; braces = [`${prefix}{`, '}'];
formatter = formatMap; formatter = formatMap;
} else if (isTypedArray(value)) { } else if (isTypedArray(value)) {
keys = getOwnNonIndexProperties(value, filter);
braces = [`${getPrefix(constructor, tag)}[`, ']']; braces = [`${getPrefix(constructor, tag)}[`, ']'];
if (value.length === 0 && keyLength === 0 && !ctx.showHidden) if (value.length === 0 && keys.length === 0 && !ctx.showHidden)
return `${braces[0]}]`; return `${braces[0]}]`;
formatter = formatTypedArray; formatter = formatTypedArray;
extrasType = kArrayExtrasType;
} else if (isMapIterator(value)) { } else if (isMapIterator(value)) {
keys = getKeys(value, ctx.showHidden);
braces = [`[${tag}] {`, '}']; braces = [`[${tag}] {`, '}'];
formatter = formatMapIterator; formatter = formatMapIterator;
} else if (isSetIterator(value)) { } else if (isSetIterator(value)) {
keys = getKeys(value, ctx.showHidden);
braces = [`[${tag}] {`, '}']; braces = [`[${tag}] {`, '}'];
formatter = formatSetIterator; formatter = formatSetIterator;
} else { } else {
@ -693,34 +731,35 @@ function formatValue(ctx, value, recurseTimes) {
} }
} }
if (noIterator) { if (noIterator) {
keys = getKeys(value, ctx.showHidden);
braces = ['{', '}']; braces = ['{', '}'];
if (constructor === 'Object') { if (constructor === 'Object') {
if (isArgumentsObject(value)) { if (isArgumentsObject(value)) {
if (keyLength === 0) if (keys.length === 0)
return '[Arguments] {}'; return '[Arguments] {}';
braces[0] = '[Arguments] {'; braces[0] = '[Arguments] {';
} else if (tag !== '') { } else if (tag !== '') {
braces[0] = `${getPrefix(constructor, tag)}{`; braces[0] = `${getPrefix(constructor, tag)}{`;
if (keyLength === 0) { if (keys.length === 0) {
return `${braces[0]}}`; return `${braces[0]}}`;
} }
} else if (keyLength === 0) { } else if (keys.length === 0) {
return '{}'; return '{}';
} }
} else if (typeof value === 'function') { } else if (typeof value === 'function') {
const type = constructor || tag || 'Function'; const type = constructor || tag || 'Function';
const name = `${type}${value.name ? `: ${value.name}` : ''}`; const name = `${type}${value.name ? `: ${value.name}` : ''}`;
if (keyLength === 0) if (keys.length === 0)
return ctx.stylize(`[${name}]`, 'special'); return ctx.stylize(`[${name}]`, 'special');
base = `[${name}]`; base = `[${name}]`;
} else if (isRegExp(value)) { } else if (isRegExp(value)) {
// Make RegExps say that they are RegExps // Make RegExps say that they are RegExps
if (keyLength === 0 || recurseTimes < 0) if (keys.length === 0 || recurseTimes < 0)
return ctx.stylize(regExpToString(value), 'regexp'); return ctx.stylize(regExpToString(value), 'regexp');
base = `${regExpToString(value)}`; base = `${regExpToString(value)}`;
} else if (isDate(value)) { } else if (isDate(value)) {
// Make dates with properties first say the date // Make dates with properties first say the date
if (keyLength === 0) { if (keys.length === 0) {
if (Number.isNaN(dateGetTime(value))) if (Number.isNaN(dateGetTime(value)))
return ctx.stylize(String(value), 'date'); return ctx.stylize(String(value), 'date');
return ctx.stylize(dateToISOString(value), 'date'); return ctx.stylize(dateToISOString(value), 'date');
@ -739,7 +778,7 @@ function formatValue(ctx, value, recurseTimes) {
const indentation = ' '.repeat(ctx.indentationLvl); const indentation = ' '.repeat(ctx.indentationLvl);
base = formatError(value).replace(/\n/g, `\n${indentation}`); base = formatError(value).replace(/\n/g, `\n${indentation}`);
} }
if (keyLength === 0) if (keys.length === 0)
return base; return base;
if (ctx.compact === false && stackStart !== -1) { if (ctx.compact === false && stackStart !== -1) {
@ -747,14 +786,14 @@ function formatValue(ctx, value, recurseTimes) {
base = `[${base.slice(0, stackStart)}]`; base = `[${base.slice(0, stackStart)}]`;
} }
} else if (isAnyArrayBuffer(value)) { } 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); let prefix = getPrefix(constructor, tag);
if (prefix === '') { if (prefix === '') {
prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer '; 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 + return prefix +
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; `{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
braces[0] = `${prefix}{`; braces[0] = `${prefix}{`;
@ -768,50 +807,42 @@ function formatValue(ctx, value, recurseTimes) {
formatter = formatPromise; formatter = formatPromise;
} else if (isWeakSet(value)) { } else if (isWeakSet(value)) {
braces[0] = `${getPrefix(constructor, tag, 'WeakSet')}{`; braces[0] = `${getPrefix(constructor, tag, 'WeakSet')}{`;
if (ctx.showHidden) { formatter = ctx.showHidden ? formatWeakSet : formatWeakCollection;
formatter = formatWeakSet;
} else {
extra = ctx.stylize('<items unknown>', 'special');
}
} else if (isWeakMap(value)) { } else if (isWeakMap(value)) {
braces[0] = `${getPrefix(constructor, tag, 'WeakMap')}{`; braces[0] = `${getPrefix(constructor, tag, 'WeakMap')}{`;
if (ctx.showHidden) { formatter = ctx.showHidden ? formatWeakMap : formatWeakCollection;
formatter = formatWeakMap;
} else {
extra = ctx.stylize('<items unknown>', 'special');
}
} else if (types.isModuleNamespaceObject(value)) { } else if (types.isModuleNamespaceObject(value)) {
braces[0] = `[${tag}] {`; braces[0] = `[${tag}] {`;
formatter = formatNamespaceObject; formatter = formatNamespaceObject;
} else if (isNumberObject(value)) { skip = true;
base = `[Number: ${getBoxedValue(numberValueOf(value))}]`; } else if (isBoxedPrimitive(value)) {
if (keyLength === 0) let type;
return ctx.stylize(base, 'number'); if (isNumberObject(value)) {
} else if (isBooleanObject(value)) { base = `[Number: ${getBoxedValue(numberValueOf(value))}]`;
base = `[Boolean: ${getBoxedValue(booleanValueOf(value))}]`; type = 'number';
if (keyLength === 0) } else if (isStringObject(value)) {
return ctx.stylize(base, 'boolean'); base = `[String: ${getBoxedValue(stringValueOf(value), ctx)}]`;
} else if (isBigIntObject(value)) { type = 'string';
base = `[BigInt: ${getBoxedValue(bigIntValueOf(value))}]`; // For boxed Strings, we have to remove the 0-n indexed entries,
if (keyLength === 0) // since they just noisy up the output and are redundant
return ctx.stylize(base, 'bigint'); // Make boxed primitive Strings look like such
} else if (isSymbolObject(value)) { keys = keys.slice(value.length);
base = `[Symbol: ${getBoxedValue(symbolValueOf(value))}]`; } else if (isBooleanObject(value)) {
if (keyLength === 0) base = `[Boolean: ${getBoxedValue(booleanValueOf(value))}]`;
return ctx.stylize(base, 'symbol'); type = 'boolean';
} else if (isStringObject(value)) { } else if (isBigIntObject(value)) {
const raw = stringValueOf(value); base = `[BigInt: ${getBoxedValue(bigIntValueOf(value))}]`;
base = `[String: ${getBoxedValue(raw, ctx)}]`; type = 'bigint';
if (keyLength === raw.length) } else {
return ctx.stylize(base, 'string'); base = `[Symbol: ${getBoxedValue(symbolValueOf(value))}]`;
// For boxed Strings, we have to remove the 0-n indexed entries, type = 'symbol';
// since they just noisy up the output and are redundant }
// Make boxed primitive Strings look like such if (keys.length === 0) {
keys = keys.slice(value.length); return ctx.stylize(base, type);
braces = ['{', '}']; }
// The input prototype got manipulated. Special handle these.
// We have to rebuild the information so we are able to display everything.
} else { } 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); const specialIterator = noPrototypeIterator(ctx, value, recurseTimes);
if (specialIterator) { if (specialIterator) {
return specialIterator; return specialIterator;
@ -823,7 +854,7 @@ function formatValue(ctx, value, recurseTimes) {
braces = [`[${tag || 'Set Iterator'}] {`, '}']; braces = [`[${tag || 'Set Iterator'}] {`, '}'];
formatter = formatSetIterator; formatter = formatSetIterator;
// Handle other regular objects again. // Handle other regular objects again.
} else if (keyLength === 0) { } else if (keys.length === 0) {
if (isExternal(value)) if (isExternal(value))
return ctx.stylize('[External]', 'special'); return ctx.stylize('[External]', 'special');
return `${getPrefix(constructor, tag)}{}`; return `${getPrefix(constructor, tag)}{}`;
@ -841,36 +872,34 @@ function formatValue(ctx, value, recurseTimes) {
ctx.seen.push(value); ctx.seen.push(value);
let output; let output;
// This corresponds to a depth of at least 333 and likely 500. try {
if (ctx.indentationLvl < 1000) {
output = formatter(ctx, value, recurseTimes, keys); output = formatter(ctx, value, recurseTimes, keys);
} else { if (skip === false) {
try { for (i = 0; i < keys.length; i++) {
output = formatter(ctx, value, recurseTimes, keys); output.push(
} catch (err) { formatProperty(ctx, value, recurseTimes, keys[i], extrasType));
if (errors.isStackOverflowError(err)) {
ctx.seen.pop();
return ctx.stylize(
`[${constructor || tag || 'Object'}: Inspection interrupted ` +
'prematurely. Maximum call stack size exceeded.]',
'special'
);
} }
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(); ctx.seen.pop();
return reduceToSingleString(ctx, output, base, braces); 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) { function formatNumber(fn, value) {
// Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0. // Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0.
if (Object.is(value, -0)) if (Object.is(value, -0))
@ -935,20 +964,13 @@ function formatError(value) {
return value.stack || errorToString(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) { function formatNamespaceObject(ctx, value, recurseTimes, keys) {
const len = keys.length; const len = keys.length;
const output = new Array(len); const output = new Array(len);
for (var i = 0; i < len; i++) { for (var i = 0; i < len; i++) {
try { try {
output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 0); output[i] = formatProperty(ctx, value, recurseTimes, keys[i],
kObjectType);
} catch (err) { } catch (err) {
if (!(types.isNativeError(err) && err.name === 'ReferenceError')) { if (!(types.isNativeError(err) && err.name === 'ReferenceError')) {
throw err; throw err;
@ -957,7 +979,7 @@ function formatNamespaceObject(ctx, value, recurseTimes, keys) {
// line breaks are always correct. Otherwise it is very difficult to keep // 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. // this aligned, even though this is a hacky way of dealing with this.
const tmp = { [keys[i]]: '' }; 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(' '); const pos = output[i].lastIndexOf(' ');
// We have to find the last whitespace and have to replace that value as // We have to find the last whitespace and have to replace that value as
// it will be visualized as a regular string. // it will be visualized as a regular string.
@ -969,91 +991,67 @@ function formatNamespaceObject(ctx, value, recurseTimes, keys) {
} }
// The array is sparse and/or has extra keys // The array is sparse and/or has extra keys
function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) { function formatSpecialArray(ctx, value, recurseTimes, maxLength, output, i) {
const output = []; const keys = Object.keys(value);
const keyLen = keys.length; let index = i;
let i = 0; for (; i < keys.length && output.length < maxLength; i++) {
for (const key of keys) { const key = keys[i];
if (output.length === maxLength) const tmp = +key;
break;
const index = +key;
// Arrays can only have up to 2^32 - 1 entries // Arrays can only have up to 2^32 - 1 entries
if (index > 2 ** 32 - 2) if (tmp > 2 ** 32 - 2) {
break; break;
if (`${i}` !== key) { }
if (!numberRegExp.test(key)) if (`${index}` !== key) {
if (!numberRegExp.test(key)) {
break; break;
const emptyItems = index - i; }
const emptyItems = tmp - index;
const ending = emptyItems > 1 ? 's' : ''; const ending = emptyItems > 1 ? 's' : '';
const message = `<${emptyItems} empty item${ending}>`; const message = `<${emptyItems} empty item${ending}>`;
output.push(ctx.stylize(message, 'undefined')); output.push(ctx.stylize(message, 'undefined'));
i = index; index = tmp;
if (output.length === maxLength) if (output.length === maxLength) {
break; break;
}
} }
output.push(formatProperty(ctx, value, recurseTimes, key, 1)); output.push(formatProperty(ctx, value, recurseTimes, key, kArrayType));
i++; index++;
} }
if (i < valLen && output.length !== maxLength) { const remaining = value.length - index;
const len = valLen - i; if (output.length !== maxLength) {
const ending = len > 1 ? 's' : ''; if (remaining > 0) {
const message = `<${len} empty item${ending}>`; const ending = remaining > 1 ? 's' : '';
output.push(ctx.stylize(message, 'undefined')); const message = `<${remaining} empty item${ending}>`;
i = valLen; output.push(ctx.stylize(message, 'undefined'));
if (keyLen === 0) }
return output; } else if (remaining > 0) {
}
const remaining = valLen - i;
if (remaining > 0) {
output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`);
} }
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));
}
for (i = extra.length - 1; i >= 0; i--)
output.push(extra[i]);
}
return output; return output;
} }
function formatArray(ctx, value, recurseTimes, keys) { function formatArray(ctx, value, recurseTimes) {
const len = Math.min(Math.max(0, ctx.maxArrayLength), value.length);
const hidden = ctx.showHidden ? 1 : 0;
const valLen = value.length; const valLen = value.length;
const keyLen = keys.length - hidden; const len = Math.min(Math.max(0, ctx.maxArrayLength), valLen);
if (keyLen !== valLen || keys[keyLen - 1] !== `${valLen - 1}`)
return formatSpecialArray(ctx, value, recurseTimes, keys, len, valLen);
const remaining = valLen - len; const remaining = valLen - len;
const output = new Array(len + (remaining > 0 ? 1 : 0) + hidden); const output = [];
for (var i = 0; i < len; i++) for (var i = 0; i < len; i++) {
output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 1); // 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) if (remaining > 0)
output[i++] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`; output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`);
if (ctx.showHidden === true)
output[i] = formatProperty(ctx, value, recurseTimes, 'length', 2);
return output; 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 maxLength = Math.min(Math.max(0, ctx.maxArrayLength), value.length);
const remaining = value.length - maxLength; const remaining = value.length - maxLength;
const output = new Array(maxLength + (remaining > 0 ? 1 : 0)); const output = new Array(maxLength);
const elementFormatter = value.length > 0 && typeof value[0] === 'number' ? const elementFormatter = value.length > 0 && typeof value[0] === 'number' ?
formatNumber : formatNumber :
formatBigInt; formatBigInt;
@ -1076,52 +1074,39 @@ function formatTypedArray(ctx, value, recurseTimes, keys) {
} }
ctx.indentationLvl -= 2; 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; return output;
} }
function formatSet(ctx, value, recurseTimes, keys) { function formatSet(ctx, value, recurseTimes) {
const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); const output = [];
let i = 0;
ctx.indentationLvl += 2; ctx.indentationLvl += 2;
for (const v of value) { for (const v of value) {
output[i++] = formatValue(ctx, v, recurseTimes); output.push(formatValue(ctx, v, recurseTimes));
} }
ctx.indentationLvl -= 2; ctx.indentationLvl -= 2;
// With `showHidden`, `length` will display as a hidden property for // With `showHidden`, `length` will display as a hidden property for
// arrays. For consistency's sake, do the same for `size`, even though this // arrays. For consistency's sake, do the same for `size`, even though this
// property isn't selected by Object.getOwnPropertyNames(). // property isn't selected by Object.getOwnPropertyNames().
if (ctx.showHidden) if (ctx.showHidden)
output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; output.push(`[size]: ${ctx.stylize(`${value.size}`, 'number')}`);
for (var n = 0; n < keys.length; n++) {
output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0);
}
return output; return output;
} }
function formatMap(ctx, value, recurseTimes, keys) { function formatMap(ctx, value, recurseTimes) {
const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); const output = [];
let i = 0;
ctx.indentationLvl += 2; ctx.indentationLvl += 2;
for (const [k, v] of value) { for (const [k, v] of value) {
output[i++] = `${formatValue(ctx, k, recurseTimes)} => ` + output.push(`${formatValue(ctx, k, recurseTimes)} => ` +
formatValue(ctx, v, recurseTimes); formatValue(ctx, v, recurseTimes));
} }
ctx.indentationLvl -= 2; ctx.indentationLvl -= 2;
// See comment in formatSet // See comment in formatSet
if (ctx.showHidden) if (ctx.showHidden)
output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; output.push(`[size]: ${ctx.stylize(`${value.size}`, 'number')}`);
for (var n = 0; n < keys.length; n++) {
output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0);
}
return output; return output;
} }
function formatSetIterInner(ctx, value, recurseTimes, keys, entries, state) { function formatSetIterInner(ctx, recurseTimes, entries, state) {
const maxArrayLength = Math.max(ctx.maxArrayLength, 0); const maxArrayLength = Math.max(ctx.maxArrayLength, 0);
const maxLength = Math.min(maxArrayLength, entries.length); const maxLength = Math.min(maxArrayLength, entries.length);
let output = new Array(maxLength); let output = new Array(maxLength);
@ -1139,12 +1124,10 @@ function formatSetIterInner(ctx, value, recurseTimes, keys, entries, state) {
if (remaining > 0) { if (remaining > 0) {
output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); 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; return output;
} }
function formatMapIterInner(ctx, value, recurseTimes, keys, entries, state) { function formatMapIterInner(ctx, recurseTimes, entries, state) {
const maxArrayLength = Math.max(ctx.maxArrayLength, 0); const maxArrayLength = Math.max(ctx.maxArrayLength, 0);
// Entries exist as [key1, val1, key2, val2, ...] // Entries exist as [key1, val1, key2, val2, ...]
const len = entries.length / 2; const len = entries.length / 2;
@ -1175,37 +1158,38 @@ function formatMapIterInner(ctx, value, recurseTimes, keys, entries, state) {
if (remaining > 0) { if (remaining > 0) {
output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); 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; return output;
} }
function formatWeakSet(ctx, value, recurseTimes, keys) { function formatWeakCollection(ctx) {
const entries = previewEntries(value); return [ctx.stylize('<items unknown>', 'special')];
return formatSetIterInner(ctx, value, recurseTimes, keys, entries, kWeak);
} }
function formatWeakMap(ctx, value, recurseTimes, keys) { function formatWeakSet(ctx, value, recurseTimes) {
const entries = previewEntries(value); const entries = previewEntries(value);
return formatMapIterInner(ctx, value, recurseTimes, keys, entries, kWeak); return formatSetIterInner(ctx, recurseTimes, entries, kWeak);
} }
function formatSetIterator(ctx, value, recurseTimes, keys) { function formatWeakMap(ctx, value, recurseTimes) {
const entries = previewEntries(value); const entries = previewEntries(value);
return formatSetIterInner(ctx, value, recurseTimes, keys, entries, kIterator); return formatMapIterInner(ctx, recurseTimes, entries, kWeak);
} }
function formatMapIterator(ctx, value, recurseTimes, keys) { function formatSetIterator(ctx, value, recurseTimes) {
const entries = previewEntries(value);
return formatSetIterInner(ctx, recurseTimes, entries, kIterator);
}
function formatMapIterator(ctx, value, recurseTimes) {
const [entries, isKeyValue] = previewEntries(value, true); const [entries, isKeyValue] = previewEntries(value, true);
if (isKeyValue) { if (isKeyValue) {
return formatMapIterInner( return formatMapIterInner(ctx, recurseTimes, entries, kMapEntries);
ctx, value, recurseTimes, keys, 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; let output;
const [state, result] = getPromiseDetails(value); const [state, result] = getPromiseDetails(value);
if (state === kPending) { if (state === kPending) {
@ -1222,19 +1206,16 @@ function formatPromise(ctx, value, recurseTimes, keys) {
str str
]; ];
} }
for (var n = 0; n < keys.length; n++) {
output.push(formatProperty(ctx, value, recurseTimes, keys[n], 0));
}
return output; return output;
} }
function formatProperty(ctx, value, recurseTimes, key, array) { function formatProperty(ctx, value, recurseTimes, key, type) {
let name, str; let name, str;
let extra = ' '; let extra = ' ';
const desc = Object.getOwnPropertyDescriptor(value, key) || const desc = Object.getOwnPropertyDescriptor(value, key) ||
{ value: value[key], enumerable: true }; { value: value[key], enumerable: true };
if (desc.value !== undefined) { 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; ctx.indentationLvl += diff;
str = formatValue(ctx, desc.value, recurseTimes); str = formatValue(ctx, desc.value, recurseTimes);
if (diff === 3) { if (diff === 3) {
@ -1255,7 +1236,7 @@ function formatProperty(ctx, value, recurseTimes, key, array) {
} else { } else {
str = ctx.stylize('undefined', 'undefined'); str = ctx.stylize('undefined', 'undefined');
} }
if (array === 1) { if (type === kArrayType) {
return str; return str;
} }
if (typeof key === 'symbol') { if (typeof key === 'symbol') {

View File

@ -872,7 +872,7 @@ if (typeof Symbol !== 'undefined') {
const set = new Set(['foo']); const set = new Set(['foo']);
set.bar = 42; set.bar = 42;
assert.strictEqual( assert.strictEqual(
util.inspect(set, true), util.inspect(set, { showHidden: true }),
"Set { 'foo', [size]: 1, bar: 42 }" "Set { 'foo', [size]: 1, bar: 42 }"
); );
} }