From c5fc61fc0fdea6ba062449cf55dfd855b57cb4d9 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Wed, 16 Aug 2017 21:36:52 -0300 Subject: [PATCH] util: refactor inspect for performance The main optimizations are - Removed visibleKeys - Removed proxy cache - Removed Object.assign - No key concatenating anymore - No key recalculating anymore - Improved indentation logic - Improved string escape logic - Added many fast paths - Optimized code branches a lot - Optimized (boxed) primitive handling - Inline code if possible - Only check extra keys if necessary - Guard against unnecessary more expensive calls This also fixes a bug with special array number keys as e.g. "00". Besides that there were lots of smaller optimizations, the code got a bit cleaned up and a few more tests got in. Fixes #15288 --- lib/util.js | 979 +++++++++++------------ test/parallel/test-util-format.js | 16 + test/parallel/test-util-inspect-proxy.js | 4 +- test/parallel/test-util-inspect.js | 64 +- 4 files changed, 558 insertions(+), 505 deletions(-) diff --git a/lib/util.js b/lib/util.js index 61b7a9d87ea268..ec54857b6aa376 100644 --- a/lib/util.js +++ b/lib/util.js @@ -39,8 +39,8 @@ const { isSet, isSetIterator, isTypedArray, - isRegExp: _isRegExp, - isDate: _isDate, + isRegExp, + isDate, kPending, kRejected, } = process.binding('util'); @@ -50,7 +50,8 @@ const { deprecate, getConstructorOf, isError, - promisify + promisify, + join } = require('internal/util'); const inspectDefaultOptions = Object.seal({ @@ -63,8 +64,6 @@ const inspectDefaultOptions = Object.seal({ breakLength: 60 }); -const numbersOnlyRE = /^\d+$/; - const propertyIsEnumerable = Object.prototype.propertyIsEnumerable; const regExpToString = RegExp.prototype.toString; const dateToISOString = Date.prototype.toISOString; @@ -73,6 +72,92 @@ const errorToString = Error.prototype.toString; var CIRCULAR_ERROR_MESSAGE; var Debug; +/* eslint-disable */ +const strEscapeSequencesRegExp = /[\x00-\x1f\x27\x5c]/; +const keyEscapeSequencesRegExp = /[\x00-\x1f\x27]/; +const strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g; +const keyEscapeSequencesReplacer = /[\x00-\x1f\x27]/g; +/* eslint-enable */ +const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; +const colorRegExp = /\u001b\[\d\d?m/g; +const numberRegExp = /^(0|[1-9][0-9]*)$/; + +// Escaped special characters. Use empty strings to fill up unused entries. +const meta = [ + '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', + '\\u0005', '\\u0006', '\\u0007', '\\b', '\\t', + '\\n', '\\u000b', '\\f', '\\r', '\\u000e', + '\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013', + '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', + '\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d', + '\\u001e', '\\u001f', '', '', '', + '', '', '', '', "\\'", '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '\\\\' +]; + +const escapeFn = (str) => meta[str.charCodeAt(0)]; + +// Escape control characters, single quotes and the backslash. +// This is similar to JSON stringify escaping. +function strEscape(str) { + // Some magic numbers that worked out fine while benchmarking with v8 6.0 + if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) + return `'${str}'`; + if (str.length > 100) + return `'${str.replace(strEscapeSequencesReplacer, escapeFn)}'`; + var result = ''; + var last = 0; + for (var i = 0; i < str.length; i++) { + const point = str.charCodeAt(i); + if (point === 39 || point === 92 || point < 32) { + if (last === i) { + result += meta[point]; + } else { + result += `${str.slice(last, i)}${meta[point]}`; + } + last = i + 1; + } + } + if (last === 0) { + result = str; + } else if (last !== i) { + result += str.slice(last); + } + return `'${result}'`; +} + +// Escape control characters and single quotes. +// Note: for performance reasons this is not combined with strEscape +function keyEscape(str) { + if (str.length < 5000 && !keyEscapeSequencesRegExp.test(str)) + return `'${str}'`; + if (str.length > 100) + return `'${str.replace(keyEscapeSequencesReplacer, escapeFn)}'`; + var result = ''; + var last = 0; + for (var i = 0; i < str.length; i++) { + const point = str.charCodeAt(i); + if (point === 39 || point < 32) { + if (last === i) { + result += meta[point]; + } else { + result += `${str.slice(last, i)}${meta[point]}`; + } + last = i + 1; + } + } + if (last === 0) { + result = str; + } else if (last !== i) { + result += str.slice(last); + } + return `'${result}'`; +} + function tryStringify(arg) { try { return JSON.stringify(arg); @@ -164,7 +249,6 @@ function format(f) { return str; } - var debugs = {}; var debugEnviron; @@ -188,7 +272,6 @@ function debuglog(set) { return debugs[set]; } - /** * Echos the value of a value. Tries to print the value out * in the best way possible given the different types. @@ -196,26 +279,39 @@ function debuglog(set) { * @param {Object} obj The object to print out. * @param {Object} opts Optional options object that alters the output. */ -/* legacy: obj, showHidden, depth, colors*/ +/* Legacy: obj, showHidden, depth, colors*/ function inspect(obj, opts) { - // default options - var ctx = { + // Default options + const ctx = { seen: [], - stylize: stylizeNoColor + stylize: stylizeNoColor, + showHidden: inspectDefaultOptions.showHidden, + depth: inspectDefaultOptions.depth, + colors: inspectDefaultOptions.colors, + customInspect: inspectDefaultOptions.customInspect, + showProxy: inspectDefaultOptions.showProxy, + maxArrayLength: inspectDefaultOptions.maxArrayLength, + breakLength: inspectDefaultOptions.breakLength, + indentationLvl: 0 }; - // legacy... - if (arguments.length >= 3 && arguments[2] !== undefined) { - ctx.depth = arguments[2]; - } - if (arguments.length >= 4 && arguments[3] !== undefined) { - ctx.colors = arguments[3]; + // Legacy... + if (arguments.length > 2) { + if (arguments[2] !== undefined) { + ctx.depth = arguments[2]; + } + if (arguments.length > 3 && arguments[3] !== undefined) { + ctx.colors = arguments[3]; + } } + // Set user-specified options if (typeof opts === 'boolean') { - // legacy... ctx.showHidden = opts; + } else if (opts) { + const optKeys = Object.keys(opts); + for (var i = 0; i < optKeys.length; i++) { + ctx[optKeys[i]] = opts[optKeys[i]]; + } } - // Set default and user-specified options - ctx = Object.assign({}, inspect.defaultOptions, ctx, opts); if (ctx.colors) ctx.stylize = stylizeWithColor; if (ctx.maxArrayLength === null) ctx.maxArrayLength = Infinity; return formatValue(ctx, obj, ctx.depth); @@ -223,10 +319,10 @@ function inspect(obj, opts) { inspect.custom = customInspectSymbol; Object.defineProperty(inspect, 'defaultOptions', { - get: function() { + get() { return inspectDefaultOptions; }, - set: function(options) { + set(options) { if (options === null || typeof options !== 'object') { throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'object'); } @@ -267,34 +363,18 @@ inspect.styles = Object.assign(Object.create(null), { }); function stylizeWithColor(str, styleType) { - var style = inspect.styles[styleType]; - - if (style) { - return `\u001b[${inspect.colors[style][0]}m${str}` + - `\u001b[${inspect.colors[style][1]}m`; - } else { - return str; + const style = inspect.styles[styleType]; + if (style !== undefined) { + const color = inspect.colors[style]; + return `\u001b[${color[0]}m${str}\u001b[${color[1]}m`; } + return str; } - function stylizeNoColor(str, styleType) { return str; } - -function arrayToHash(array) { - var hash = Object.create(null); - - for (var i = 0; i < array.length; i++) { - var val = array[i]; - hash[val] = true; - } - - return hash; -} - - function ensureDebugIsInitialized() { if (Debug === undefined) { const runInDebugContext = require('vm').runInDebugContext; @@ -302,51 +382,37 @@ function ensureDebugIsInitialized() { } } +function formatValue(ctx, value, recurseTimes, ln) { + // Primitive types cannot have properties + if (typeof value !== 'object' && typeof value !== 'function') { + return formatPrimitive(ctx.stylize, value); + } + if (value === null) { + return ctx.stylize('null', 'null'); + } -function formatValue(ctx, value, recurseTimes) { - if (ctx.showProxy && - ((typeof value === 'object' && value !== null) || - typeof value === 'function')) { - var proxy = undefined; - var proxyCache = ctx.proxyCache; - if (!proxyCache) - proxyCache = ctx.proxyCache = new Map(); - // Determine if we've already seen this object and have - // determined that it either is or is not a proxy. - if (proxyCache.has(value)) { - // We've seen it, if the value is not undefined, it's a Proxy. - proxy = proxyCache.get(value); - } else { - // Haven't seen it. Need to check. - // If it's not a Proxy, this will return undefined. - // Otherwise, it'll return an array. The first item - // is the target, the second item is the handler. - // We ignore (and do not return) the Proxy isRevoked property. - proxy = getProxyDetails(value); - if (proxy) { - // We know for a fact that this isn't a Proxy. - // Mark it as having already been evaluated. - // We do this because this object is passed - // recursively to formatValue below in order - // for it to get proper formatting, and because - // the target and handle objects also might be - // proxies... it's unfortunate but necessary. - proxyCache.set(proxy, undefined); + if (ctx.showProxy) { + const proxy = getProxyDetails(value); + if (proxy !== undefined) { + if (recurseTimes != null) { + if (recurseTimes < 0) + return ctx.stylize('Proxy [Array]', 'special'); + recurseTimes -= 1; } - // If the object is not a Proxy, then this stores undefined. - // This tells the code above that we've already checked and - // ruled it out. If the object is a proxy, this caches the - // results of the getProxyDetails call. - proxyCache.set(value, proxy); - } - if (proxy) { - return `Proxy ${formatValue(ctx, proxy, recurseTimes)}`; + 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}`; } } // Provide a hook for user-specified inspect functions. // Check that value is an object with an inspect function on it - if (ctx.customInspect && value) { + if (ctx.customInspect) { const maybeCustomInspect = value[customInspectSymbol] || value.inspect; if (typeof maybeCustomInspect === 'function' && @@ -354,525 +420,449 @@ function formatValue(ctx, value, recurseTimes) { maybeCustomInspect !== exports.inspect && // Also filter out any prototype objects using the circular check. !(value.constructor && value.constructor.prototype === value)) { - let ret = maybeCustomInspect.call(value, recurseTimes, ctx); + const ret = maybeCustomInspect.call(value, recurseTimes, ctx); // If the custom inspection method returned `this`, don't go into // infinite recursion. if (ret !== value) { if (typeof ret !== 'string') { - ret = formatValue(ctx, ret, recurseTimes); + return formatValue(ctx, ret, recurseTimes); } return ret; } } } - // Primitive types cannot have properties - var primitive = formatPrimitive(ctx, value); - if (primitive) { - return primitive; - } + var keys; + var symbols = Object.getOwnPropertySymbols(value); // Look up the keys of the object. - var keys = Object.keys(value); - var visibleKeys = arrayToHash(keys); - const symbolKeys = Object.getOwnPropertySymbols(value); - const enumSymbolKeys = symbolKeys - .filter((key) => propertyIsEnumerable.call(value, key)); - keys = keys.concat(enumSymbolKeys); - if (ctx.showHidden) { - keys = Object.getOwnPropertyNames(value).concat(symbolKeys); - } - - // This could be a boxed primitive (new String(), etc.), check valueOf() - // NOTE: Avoid calling `valueOf` on `Date` instance because it will return - // a number which, when object has some additional user-stored `keys`, - // will be printed out. - var formatted; - var raw = value; - try { - // the .valueOf() call can fail for a multitude of reasons - if (!isDate(value)) - raw = value.valueOf(); - } catch (e) { - // ignore... - } - - if (typeof raw === 'string') { - // for boxed Strings, we have to remove the 0-n indexed entries, - // since they just noisy up the output and are redundant - keys = keys.filter(function(key) { - if (typeof key === 'symbol') { - return true; - } - - return !(key >= 0 && key < raw.length); - }); + keys = Object.getOwnPropertyNames(value); + } else { + keys = Object.keys(value); + if (symbols.length !== 0) + symbols = symbols.filter((key) => propertyIsEnumerable.call(value, key)); } - var constructor = getConstructorOf(value); + const keyLength = keys.length + symbols.length; + const constructor = getConstructorOf(value); + const ctorName = constructor && constructor.name ? + `${constructor.name} ` : ''; - // Some type of object without properties can be shortcutted. - if (keys.length === 0) { - if (typeof value === 'function') { - const ctorName = constructor ? constructor.name : 'Function'; - return ctx.stylize( - `[${ctorName}${value.name ? `: ${value.name}` : ''}]`, 'special'); - } - if (isRegExp(value)) { - return ctx.stylize(regExpToString.call(value), 'regexp'); - } - if (isDate(value)) { - if (Number.isNaN(value.getTime())) { - return ctx.stylize(value.toString(), 'date'); + var base = ''; + var formatter = formatObject; + var braces; + var noIterator = true; + var raw; + + // Iterators and the rest are split to reduce checks + if (value[Symbol.iterator]) { + noIterator = false; + if (Array.isArray(value)) { + // Only set the constructor for non ordinary ("Array [...]") arrays. + braces = [`${ctorName === 'Array ' ? '' : ctorName}[`, ']']; + if (value.length === 0 && keyLength === 0) + return `${braces[0]}]`; + formatter = formatArray; + } else if (isSet(value)) { + if (value.size === 0 && keyLength === 0) + return `${ctorName}{}`; + braces = [`${ctorName}{`, '}']; + formatter = formatSet; + } else if (isMap(value)) { + if (value.size === 0 && keyLength === 0) + return `${ctorName}{}`; + braces = [`${ctorName}{`, '}']; + formatter = formatMap; + } else if (isTypedArray(value)) { + braces = [`${ctorName}[`, ']']; + formatter = formatTypedArray; + } else if (isMapIterator(value)) { + braces = ['MapIterator {', '}']; + formatter = formatCollectionIterator; + } else if (isSetIterator(value)) { + braces = ['SetIterator {', '}']; + formatter = formatCollectionIterator; + } else { + // Check for boxed strings with valueOf() + // The .valueOf() call can fail for a multitude of reasons + try { + raw = value.valueOf(); + } catch (e) { /* ignore */ } + + if (typeof raw === 'string') { + const formatted = formatPrimitive(stylizeNoColor, raw); + if (keyLength === raw.length) + return ctx.stylize(`[String: ${formatted}]`, 'string'); + base = ` [String: ${formatted}]`; + // 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 = ['{', '}']; } else { - return ctx.stylize(dateToISOString.call(value), 'date'); + noIterator = true; } } - if (isError(value)) { - return formatError(value); - } - // now check the `raw` value to handle boxed primitives - if (typeof raw === 'string') { - formatted = formatPrimitiveNoColor(ctx, raw); - return ctx.stylize(`[String: ${formatted}]`, 'string'); - } - if (typeof raw === 'symbol') { - formatted = formatPrimitiveNoColor(ctx, raw); - return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol'); - } - if (typeof raw === 'number') { - formatted = formatPrimitiveNoColor(ctx, raw); - return ctx.stylize(`[Number: ${formatted}]`, 'number'); - } - if (typeof raw === 'boolean') { - formatted = formatPrimitiveNoColor(ctx, raw); - return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean'); - } - // 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 (isAnyArrayBuffer(value)) { - return `${constructor.name}` + - ` { byteLength: ${formatNumber(ctx, value.byteLength)} }`; - } - if (isExternal(value)) { - return ctx.stylize('[External]', 'special'); - } } - - var base = ''; - var empty = false; - var formatter = formatObject; - var braces; - - // We can't compare constructors for various objects using a comparison like - // `constructor === Array` because the object could have come from a different - // context and thus the constructor won't match. Instead we check the - // constructor names (including those up the prototype chain where needed) to - // determine object types. - if (Array.isArray(value)) { - // Unset the constructor to prevent "Array [...]" for ordinary arrays. - if (constructor && constructor.name === 'Array') - constructor = null; - braces = ['[', ']']; - empty = value.length === 0; - formatter = formatArray; - } else if (isSet(value)) { - braces = ['{', '}']; - // 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) - keys.unshift('size'); - empty = value.size === 0; - formatter = formatSet; - } else if (isMap(value)) { - braces = ['{', '}']; - // Ditto. - if (ctx.showHidden) - keys.unshift('size'); - empty = value.size === 0; - formatter = formatMap; - } else if (isAnyArrayBuffer(value)) { + if (noIterator) { braces = ['{', '}']; - keys.unshift('byteLength'); - visibleKeys.byteLength = true; - } else if (isDataView(value)) { - braces = ['{', '}']; - // .buffer goes last, it's not a primitive like the others. - keys.unshift('byteLength', 'byteOffset', 'buffer'); - visibleKeys.byteLength = true; - visibleKeys.byteOffset = true; - visibleKeys.buffer = true; - } else if (isTypedArray(value)) { - braces = ['[', ']']; - formatter = formatTypedArray; - if (ctx.showHidden) { + if (ctorName === 'Object ') { + // Object fast path + if (keyLength === 0) + return '{}'; + } else if (typeof value === 'function') { + const name = `${constructor.name}${value.name ? `: ${value.name}` : ''}`; + if (keyLength === 0) + return ctx.stylize(`[${name}]`, 'special'); + base = ` [${name}]`; + } else if (isRegExp(value)) { + // Make RegExps say that they are RegExps + if (keyLength === 0 || recurseTimes < 0) + return ctx.stylize(regExpToString.call(value), 'regexp'); + base = ` ${regExpToString.call(value)}`; + } else if (isDate(value)) { + if (keyLength === 0) { + if (Number.isNaN(value.getTime())) + return ctx.stylize(value.toString(), 'date'); + return ctx.stylize(dateToISOString.call(value), 'date'); + } + // Make dates with properties first say the date + base = ` ${dateToISOString.call(value)}`; + } else if (isError(value)) { + // Make error with message first say the error + if (keyLength === 0) + return formatError(value); + base = ` ${formatError(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. + if (keyLength === 0) + return ctorName + + `{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; + braces[0] = `${ctorName}{`; + keys.unshift('byteLength'); + } else if (isDataView(value)) { + braces[0] = `${ctorName}{`; // .buffer goes last, it's not a primitive like the others. - keys.unshift('BYTES_PER_ELEMENT', - 'length', - 'byteLength', - 'byteOffset', - 'buffer'); + keys.unshift('byteLength', 'byteOffset', 'buffer'); + } else if (isPromise(value)) { + braces[0] = `${ctorName}{`; + formatter = formatPromise; + } else { + // Check boxed primitives other than string with valueOf() + // NOTE: `Date` has to be checked first! + // The .valueOf() call can fail for a multitude of reasons + try { + raw = value.valueOf(); + } catch (e) { /* ignore */ } + + if (typeof raw === 'number') { + // Make boxed primitive Numbers look like such + const formatted = formatPrimitive(stylizeNoColor, raw); + if (keyLength === 0) + return ctx.stylize(`[Number: ${formatted}]`, 'number'); + base = ` [Number: ${formatted}]`; + } else if (typeof raw === 'boolean') { + // Make boxed primitive Booleans look like such + const formatted = formatPrimitive(stylizeNoColor, raw); + if (keyLength === 0) + return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean'); + base = ` [Boolean: ${formatted}]`; + } else if (typeof raw === 'symbol') { + const formatted = formatPrimitive(stylizeNoColor, raw); + return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol'); + } else if (keyLength === 0) { + if (isExternal(value)) + return ctx.stylize('[External]', 'special'); + return `${ctorName}{}`; + } else { + braces[0] = `${ctorName}{`; + } } - } else if (isPromise(value)) { - braces = ['{', '}']; - formatter = formatPromise; - } else if (isMapIterator(value)) { - constructor = { name: 'MapIterator' }; - braces = ['{', '}']; - empty = false; - formatter = formatCollectionIterator; - } else if (isSetIterator(value)) { - constructor = { name: 'SetIterator' }; - braces = ['{', '}']; - empty = false; - formatter = formatCollectionIterator; - } else { - // Unset the constructor to prevent "Object {...}" for ordinary objects. - if (constructor && constructor.name === 'Object') - constructor = null; - braces = ['{', '}']; - empty = true; // No other data than keys. - } - - empty = empty === true && keys.length === 0; - - // Make functions say that they are functions - if (typeof value === 'function') { - const ctorName = constructor ? constructor.name : 'Function'; - base = ` [${ctorName}${value.name ? `: ${value.name}` : ''}]`; } - // Make RegExps say that they are RegExps - if (isRegExp(value)) { - base = ` ${regExpToString.call(value)}`; - } - - // Make dates with properties first say the date - if (isDate(value)) { - base = ` ${dateToISOString.call(value)}`; - } - - // Make error with message first say the error - if (isError(value)) { - base = ` ${formatError(value)}`; - } - - // Make boxed primitive Strings look like such - if (typeof raw === 'string') { - formatted = formatPrimitiveNoColor(ctx, raw); - base = ` [String: ${formatted}]`; - } - - // Make boxed primitive Numbers look like such - if (typeof raw === 'number') { - formatted = formatPrimitiveNoColor(ctx, raw); - base = ` [Number: ${formatted}]`; - } - - // Make boxed primitive Booleans look like such - if (typeof raw === 'boolean') { - formatted = formatPrimitiveNoColor(ctx, raw); - base = ` [Boolean: ${formatted}]`; - } - - // Add constructor name if available - if (base === '' && constructor) - braces[0] = `${constructor.name} ${braces[0]}`; - - if (empty === true) { - return `${braces[0]}${base}${braces[1]}`; - } + // Using an array here is actually better for the average case than using + // a Set. `seen` will only check for the depth and will never grow to large. + if (ctx.seen.indexOf(value) !== -1) + return ctx.stylize('[Circular]', 'special'); - if (recurseTimes < 0) { - if (isRegExp(value)) { - return ctx.stylize(regExpToString.call(value), 'regexp'); - } else if (Array.isArray(value)) { - return ctx.stylize('[Array]', 'special'); - } else { + if (recurseTimes != null) { + if (recurseTimes < 0) { + if (Array.isArray(value)) + return ctx.stylize('[Array]', 'special'); return ctx.stylize('[Object]', 'special'); } - } - - // TODO(addaleax): Make `seen` a Set to avoid linear-time lookup. - if (ctx.seen.includes(value)) { - return ctx.stylize('[Circular]', 'special'); + recurseTimes -= 1; } ctx.seen.push(value); - const output = formatter(ctx, value, recurseTimes, visibleKeys, keys); + const output = formatter(ctx, value, recurseTimes, keys); + + for (var i = 0; i < symbols.length; i++) { + output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0)); + } ctx.seen.pop(); - return reduceToSingleString(output, base, braces, ctx.breakLength); + return reduceToSingleString(ctx, output, base, braces, ln); } - -function formatNumber(ctx, value) { +function formatNumber(fn, value) { // Format -0 as '-0'. Strict equality won't distinguish 0 from -0. if (Object.is(value, -0)) - return ctx.stylize('-0', 'number'); - return ctx.stylize(`${value}`, 'number'); + return fn('-0', 'number'); + return fn(`${value}`, 'number'); } - -function formatPrimitive(ctx, value) { - if (value === undefined) - return ctx.stylize('undefined', 'undefined'); - - // For some reason typeof null is "object", so special case here. - if (value === null) - return ctx.stylize('null', 'null'); - - var type = typeof value; - - if (type === 'string') { - var simple = JSON.stringify(value) - .replace(/^"|"$/g, '') - .replace(/'/g, "\\'") - .replace(/\\"/g, '"'); - return ctx.stylize(`'${simple}'`, 'string'); - } - if (type === 'number') - return formatNumber(ctx, value); - if (type === 'boolean') - return ctx.stylize(`${value}`, 'boolean'); +function formatPrimitive(fn, value) { + if (typeof value === 'string') + return fn(strEscape(value), 'string'); + if (typeof value === 'number') + return formatNumber(fn, value); + if (typeof value === 'boolean') + return fn(`${value}`, 'boolean'); + if (typeof value === 'undefined') + return fn('undefined', 'undefined'); // es6 symbol primitive - if (type === 'symbol') - return ctx.stylize(value.toString(), 'symbol'); + return fn(value.toString(), 'symbol'); } - -function formatPrimitiveNoColor(ctx, value) { - var stylize = ctx.stylize; - ctx.stylize = stylizeNoColor; - var str = formatPrimitive(ctx, value); - ctx.stylize = stylize; - return str; -} - - function formatError(value) { return value.stack || `[${errorToString.call(value)}]`; } - -function formatObject(ctx, value, recurseTimes, visibleKeys, keys) { - return keys.map(function(key) { - return formatProperty(ctx, value, recurseTimes, visibleKeys, key, false); - }); +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 formatArray(ctx, value, recurseTimes, visibleKeys, keys) { - const maxLength = Math.min(Math.max(0, ctx.maxArrayLength), value.length); - var output = []; - let visibleLength = 0; - let index = 0; - for (const elem of keys) { - if (visibleLength === maxLength) - break; - // Symbols might have been added to the keys - if (typeof elem !== 'string') - continue; - const i = +elem; - if (index !== i) { - // Skip zero and negative numbers as well as non numbers - if (i > 0 === false) - continue; - const emptyItems = i - index; - const ending = emptyItems > 1 ? 's' : ''; - const message = `<${emptyItems} empty item${ending}>`; - output.push(ctx.stylize(message, 'undefined')); - index = i; - if (++visibleLength === maxLength) +// The array is sparse and/or has extra keys +function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) { + const output = []; + const keyLen = keys.length; + var visibleLength = 0; + var i = 0; + if (keyLen !== 0 && numberRegExp.test(keys[0])) { + for (const key of keys) { + if (visibleLength === maxLength) break; + const index = +key; + if (i !== index) { + if (!numberRegExp.test(key)) + break; + const emptyItems = index - i; + const ending = emptyItems > 1 ? 's' : ''; + const message = `<${emptyItems} empty item${ending}>`; + output.push(ctx.stylize(message, 'undefined')); + i = index; + if (++visibleLength === maxLength) + break; + } + output.push(formatProperty(ctx, value, recurseTimes, key, 1)); + visibleLength++; + i++; } - output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, - elem, true)); - visibleLength++; - index++; } - if (index < value.length && visibleLength !== maxLength) { - const len = value.length - index; + if (i < valLen && visibleLength !== maxLength) { + const len = valLen - i; const ending = len > 1 ? 's' : ''; const message = `<${len} empty item${ending}>`; output.push(ctx.stylize(message, 'undefined')); - index = value.length; + i = valLen; } - var remaining = value.length - index; + const remaining = valLen - i; if (remaining > 0) { output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); } - for (var n = 0; n < keys.length; n++) { - var key = keys[n]; - if (typeof key === 'symbol' || !numbersOnlyRE.test(key)) { - output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, - key, true)); + 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 (keyLen !== 0 && keys[keyLen - 1] !== `${valLen - 1}`) { + for (const key of keys) { + // Skip regular indices + if (!numberRegExp.test(key)) { + output.push(formatProperty(ctx, value, recurseTimes, key, 2)); + } } } 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; + 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 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); + if (remaining > 0) + output[i++] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`; + if (ctx.showHidden === true) + output[i] = formatProperty(ctx, value, recurseTimes, 'length', 2); + return output; +} -function formatTypedArray(ctx, value, recurseTimes, visibleKeys, keys) { +function formatTypedArray(ctx, value, recurseTimes, keys) { const maxLength = Math.min(Math.max(0, ctx.maxArrayLength), value.length); const remaining = value.length - maxLength; - var output = new Array(maxLength); + const output = new Array(maxLength + (remaining > 0 ? 1 : 0)); for (var i = 0; i < maxLength; ++i) - output[i] = formatNumber(ctx, value[i]); - if (remaining > 0) { - output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); - } - for (const key of keys) { - if (typeof key === 'symbol' || !numbersOnlyRE.test(key)) { - output.push( - formatProperty(ctx, value, recurseTimes, visibleKeys, key, true)); + output[i] = formatNumber(ctx.stylize, value[i]); + if (remaining > 0) + output[i] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`; + if (ctx.showHidden) { + // .buffer goes last, it's not a primitive like the others. + const extraKeys = [ + 'BYTES_PER_ELEMENT', + 'length', + 'byteLength', + 'byteOffset', + 'buffer' + ]; + for (i = 0; i < extraKeys.length; i++) { + const str = formatValue(ctx, value[extraKeys[i]], recurseTimes); + output.push(`[${extraKeys[i]}]: ${str}`); } } + // 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, visibleKeys, keys) { - var output = []; - value.forEach(function(v) { - var nextRecurseTimes = recurseTimes === null ? null : recurseTimes - 1; - var str = formatValue(ctx, v, nextRecurseTimes); - output.push(str); - }); +function formatSet(ctx, value, recurseTimes, keys) { + const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); + var i = 0; + for (const v of value) + output[i++] = formatValue(ctx, v, recurseTimes); + // 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.push(formatProperty(ctx, value, recurseTimes, visibleKeys, - keys[n], false)); + output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); } return output; } - -function formatMap(ctx, value, recurseTimes, visibleKeys, keys) { - var output = []; - value.forEach(function(v, k) { - var nextRecurseTimes = recurseTimes === null ? null : recurseTimes - 1; - var str = formatValue(ctx, k, nextRecurseTimes); - str += ' => '; - str += formatValue(ctx, v, nextRecurseTimes); - output.push(str); - }); +function formatMap(ctx, value, recurseTimes, keys) { + const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); + var i = 0; + for (const [k, v] of value) + output[i++] = `${formatValue(ctx, k, recurseTimes)} => ` + + formatValue(ctx, v, recurseTimes); + // See comment in formatSet + if (ctx.showHidden) + output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; for (var n = 0; n < keys.length; n++) { - output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, - keys[n], false)); + output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); } return output; } -function formatCollectionIterator(ctx, value, recurseTimes, visibleKeys, keys) { +function formatCollectionIterator(ctx, value, recurseTimes, keys) { ensureDebugIsInitialized(); const mirror = Debug.MakeMirror(value, true); - var nextRecurseTimes = recurseTimes === null ? null : recurseTimes - 1; - var vals = mirror.preview(); - var output = []; + const vals = mirror.preview(); + const output = []; for (const o of vals) { - output.push(formatValue(ctx, o, nextRecurseTimes)); + output.push(formatValue(ctx, o, recurseTimes)); } return output; } -function formatPromise(ctx, value, recurseTimes, visibleKeys, keys) { - const output = []; +function formatPromise(ctx, value, recurseTimes, keys) { + var output; const [state, result] = getPromiseDetails(value); - if (state === kPending) { - output.push(''); + output = ['']; } else { - var nextRecurseTimes = recurseTimes === null ? null : recurseTimes - 1; - var str = formatValue(ctx, result, nextRecurseTimes); - if (state === kRejected) { - output.push(` ${str}`); - } else { - output.push(str); - } + const str = formatValue(ctx, result, recurseTimes); + output = [state === kRejected ? ` ${str}` : str]; } for (var n = 0; n < keys.length; n++) { - output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, - keys[n], false)); + output.push(formatProperty(ctx, value, recurseTimes, keys[n], 0)); } return output; } - -function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { - var name, str, desc; - desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; - if (desc.get) { - if (desc.set) { +function formatProperty(ctx, value, recurseTimes, key, array) { + var name, str; + const desc = Object.getOwnPropertyDescriptor(value, key) || + { value: value[key], enumerable: true }; + if (desc.value !== undefined) { + const diff = array === 0 ? 3 : 2; + ctx.indentationLvl += diff; + str = formatValue(ctx, desc.value, recurseTimes, array === 0); + ctx.indentationLvl -= diff; + } else if (desc.get !== undefined) { + if (desc.set !== undefined) { str = ctx.stylize('[Getter/Setter]', 'special'); } else { str = ctx.stylize('[Getter]', 'special'); } + } else if (desc.set !== undefined) { + str = ctx.stylize('[Setter]', 'special'); } else { - if (desc.set) { - str = ctx.stylize('[Setter]', 'special'); - } + str = ctx.stylize('undefined', 'undefined'); } - if (visibleKeys[key] === undefined) { - if (typeof key === 'symbol') { - name = `[${ctx.stylize(key.toString(), 'symbol')}]`; - } else { - name = `[${key}]`; - } - } - if (!str) { - if (recurseTimes === null) { - str = formatValue(ctx, desc.value, null); - } else { - str = formatValue(ctx, desc.value, recurseTimes - 1); - } - if (str.indexOf('\n') > -1) { - if (array) { - str = str.replace(/\n/g, '\n '); - } else { - str = str.replace(/^|\n/g, '\n '); - } - } + if (array === 1) { + return str; } - if (name === undefined) { - if (array && numbersOnlyRE.test(key)) { - return str; - } - name = JSON.stringify(`${key}`); - if (/^"[a-zA-Z_][a-zA-Z_0-9]*"$/.test(name)) { - name = name.substr(1, name.length - 2); - name = ctx.stylize(name, 'name'); - } else { - name = name.replace(/'/g, "\\'") - .replace(/\\"/g, '"') - .replace(/^"|"$/g, "'") - .replace(/\\\\/g, '\\'); - name = ctx.stylize(name, 'string'); - } + if (typeof key === 'symbol') { + name = `[${ctx.stylize(key.toString(), 'symbol')}]`; + } else if (desc.enumerable === false) { + name = `[${key}]`; + } else if (keyStrRegExp.test(key)) { + name = ctx.stylize(key, 'name'); + } else { + name = ctx.stylize(keyEscape(key), 'string'); } return `${name}: ${str}`; } - -function reduceToSingleString(output, base, braces, breakLength) { - var length = output.reduce(function(prev, cur) { - return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; - }, 0); - - if (length > breakLength) { - return braces[0] + - // If the opening "brace" is too large, like in the case of "Set {", - // we need to force the first item to be on the next line or the - // items will not line up correctly. - (base === '' && braces[0].length === 1 ? '' : `${base}\n `) + - ` ${output.join(',\n ')} ${braces[1]}`; +function reduceToSingleString(ctx, output, base, braces, addLn) { + const breakLength = ctx.breakLength; + if (output.length * 2 <= breakLength) { + var length = 0; + for (var i = 0; i < output.length && length <= breakLength; i++) { + if (ctx.colors) { + length += output[i].replace(colorRegExp, '').length + 1; + } else { + length += output[i].length + 1; + } + } + if (length <= breakLength) + return `${braces[0]}${base} ${join(output, ', ')} ${braces[1]}`; } - - return `${braces[0]}${base} ${output.join(', ')} ${braces[1]}`; + // If the opening "brace" is too large, like in the case of "Set {", + // we need to force the first item to be on the next line or the + // items will not line up correctly. + const indentation = ' '.repeat(ctx.indentationLvl); + const extraLn = addLn === true ? `\n${indentation}` : ''; + const ln = base === '' && braces[0].length === 1 ? + ' ' : `${base}\n${indentation} `; + const str = join(output, `,\n${indentation} `); + return `${extraLn}${braces[0]}${ln}${str} ${braces[1]}`; } function isBoolean(arg) { @@ -903,18 +893,10 @@ function isUndefined(arg) { return arg === undefined; } -function isRegExp(re) { - return _isRegExp(re); -} - function isObject(arg) { return arg !== null && typeof arg === 'object'; } -function isDate(d) { - return _isDate(d); -} - function isFunction(arg) { return typeof arg === 'function'; } @@ -928,7 +910,6 @@ function pad(n) { return n < 10 ? `0${n.toString(10)}` : n.toString(10); } - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; @@ -941,13 +922,11 @@ function timestamp() { return [d.getDate(), months[d.getMonth()], time].join(' '); } - // log is just a thin wrapper to console.log that prepends a timestamp function log() { console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); } - /** * Inherit the prototype methods from one constructor into another. * @@ -1030,7 +1009,6 @@ function _errnoException(err, syscall, original) { return e; } - function _exceptionWithHostPort(err, syscall, address, @@ -1072,7 +1050,6 @@ function callbackifyOnRejected(reason, cb) { return cb(reason); } - function callbackify(original) { if (typeof original !== 'function') { throw new errors.TypeError( diff --git a/test/parallel/test-util-format.js b/test/parallel/test-util-format.js index 27e3b62476211c..c601a64d48dc38 100644 --- a/test/parallel/test-util-format.js +++ b/test/parallel/test-util-format.js @@ -114,6 +114,11 @@ const nestedObj = { func: function() {} } }; +const nestedObj2 = { + foo: 'bar', + foobar: 1, + func: [{ a: function() {} }] +}; assert.strictEqual(util.format('%o'), '%o'); assert.strictEqual(util.format('%o', 42), '42'); assert.strictEqual(util.format('%o', 'foo'), '\'foo\''); @@ -126,6 +131,17 @@ assert.strictEqual( ' [length]: 0,\n' + ' [name]: \'func\',\n' + ' [prototype]: func { [constructor]: [Circular] } } }'); +assert.strictEqual( + util.format('%o', nestedObj2), + '{ foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: \n' + + ' [ { a: \n' + + ' { [Function: a]\n' + + ' [length]: 0,\n' + + ' [name]: \'a\',\n' + + ' [prototype]: a { [constructor]: [Circular] } } },\n' + + ' [length]: 1 ] }'); assert.strictEqual( util.format('%o', nestedObj), '{ foo: \'bar\',\n' + diff --git a/test/parallel/test-util-inspect-proxy.js b/test/parallel/test-util-inspect-proxy.js index ed9e9d196af03f..63527986b1cbf1 100644 --- a/test/parallel/test-util-inspect-proxy.js +++ b/test/parallel/test-util-inspect-proxy.js @@ -55,7 +55,9 @@ const expected6 = 'Proxy [ Proxy [ Proxy [ Proxy [Array], Proxy [Array]' + ' ],\n Proxy [ Proxy [Array], Proxy [Array] ] ],\n' + ' Proxy [ Proxy [ Proxy [Array], Proxy [Array] ],\n' + ' Proxy [ Proxy [Array], Proxy [Array] ] ] ]'; -assert.strictEqual(util.inspect(proxy1, opts), expected1); +assert.strictEqual( + util.inspect(proxy1, { showProxy: true, depth: null }), + expected1); assert.strictEqual(util.inspect(proxy2, opts), expected2); assert.strictEqual(util.inspect(proxy3, opts), expected3); assert.strictEqual(util.inspect(proxy4, opts), expected4); diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index f4e7bb9e5ba7ff..424e4c7504772a 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -43,14 +43,15 @@ assert.strictEqual( new Date('2010-02-14T12:48:40+01:00').toISOString() ); assert.strictEqual(util.inspect(new Date('')), (new Date('')).toString()); - assert.strictEqual(util.inspect('\n\u0001'), "'\\n\\u0001'"); - +assert.strictEqual( + util.inspect(`${Array(75).fill(1)}'\n\u001d\n\u0003`), + `'${Array(75).fill(1)}\\'\\n\\u001d\\n\\u0003'` +); assert.strictEqual(util.inspect([]), '[]'); assert.strictEqual(util.inspect(Object.create([])), 'Array {}'); assert.strictEqual(util.inspect([1, 2]), '[ 1, 2 ]'); assert.strictEqual(util.inspect([1, [2, 3]]), '[ 1, [ 2, 3 ] ]'); - assert.strictEqual(util.inspect({}), '{}'); assert.strictEqual(util.inspect({ a: 1 }), '{ a: 1 }'); assert.strictEqual(util.inspect({ a: function() {} }), '{ a: [Function: a] }'); @@ -76,6 +77,7 @@ assert.strictEqual(util.inspect({ 'a': { 'b': { 'c': 2 } } }, false, 1), '{ a: { b: [Object] } }'); assert.strictEqual(util.inspect({ 'a': { 'b': ['c'] } }, false, 1), '{ a: { b: [Array] } }'); +assert.strictEqual(util.inspect(new Uint8Array(0)), 'Uint8Array [ ]'); assert.strictEqual( util.inspect( Object.create( @@ -327,6 +329,38 @@ assert.strictEqual( assert.strictEqual(util.inspect(arr), 'CustomArray [ <50 empty items> ]'); } +// Array with extra properties +{ + const arr = [1, 2, 3, , ]; + arr.foo = 'bar'; + assert.strictEqual(util.inspect(arr), + "[ 1, 2, 3, <1 empty item>, foo: 'bar' ]"); + + const arr2 = []; + assert.strictEqual(util.inspect([], { showHidden: true }), '[ [length]: 0 ]'); + arr2['00'] = 1; + assert.strictEqual(util.inspect(arr2), "[ '00': 1 ]"); + assert.strictEqual(util.inspect(arr2, { showHidden: true }), + "[ [length]: 0, '00': 1 ]"); + arr2[1] = 0; + assert.strictEqual(util.inspect(arr2), "[ <1 empty item>, 0, '00': 1 ]"); + assert.strictEqual(util.inspect(arr2, { showHidden: true }), + "[ <1 empty item>, 0, [length]: 2, '00': 1 ]"); + delete arr2[1]; + assert.strictEqual(util.inspect(arr2), "[ <2 empty items>, '00': 1 ]"); + assert.strictEqual(util.inspect(arr2, { showHidden: true }), + "[ <2 empty items>, [length]: 2, '00': 1 ]"); + arr2['01'] = 2; + assert.strictEqual(util.inspect(arr2), + "[ <2 empty items>, '00': 1, '01': 2 ]"); + assert.strictEqual(util.inspect(arr2, { showHidden: true }), + "[ <2 empty items>, [length]: 2, '00': 1, '01': 2 ]"); + + const arr3 = []; + arr3[-1] = -1; + assert.strictEqual(util.inspect(arr3), "[ '-1': -1 ]"); +} + // Function with properties { const value = () => {}; @@ -387,6 +421,11 @@ assert.strictEqual(util.inspect(-0), '-0'); util.inspect(a, { breakLength: Infinity }), '[ \'foo\', <1 empty item>, \'baz\', \'bar\', <96 empty items>, \'qux\' ]' ); + delete a[3]; + assert.strictEqual( + util.inspect(a, { maxArrayLength: 4 }), + '[ \'foo\', <1 empty item>, \'baz\', <97 empty items>, ... 1 more item ]' + ); } // test for Array constructor in different context @@ -604,6 +643,10 @@ assert.doesNotThrow(() => { util.inspect(subject, { depth: null }).includes('{ d: 0 }'), true ); + assert.strictEqual( + util.inspect(subject, { depth: undefined }).includes('{ d: 0 }'), + true + ); } { @@ -714,6 +757,7 @@ assert.doesNotThrow(() => { testLines([1, 2, 3, 4, 5, 6, 7]); testLines(bigArray); testLines({ foo: 'bar', baz: 35, b: { a: 35 } }); + testLines({ a: { a: 3, b: 1, c: 1, d: 1, e: 1, f: 1, g: 1, h: 1 }, b: 1 }); testLines({ foo: 'bar', baz: 35, @@ -941,6 +985,20 @@ if (typeof Symbol !== 'undefined') { 'PromiseSubclass { }'); } +// Empty and circular before depth +{ + const arr = [[[[]]]]; + assert.strictEqual(util.inspect(arr), '[ [ [ [] ] ] ]'); + arr[0][0][0][0] = []; + assert.strictEqual(util.inspect(arr), '[ [ [ [Array] ] ] ]'); + arr[0][0][0] = {}; + assert.strictEqual(util.inspect(arr), '[ [ [ {} ] ] ]'); + arr[0][0][0] = { a: 2 }; + assert.strictEqual(util.inspect(arr), '[ [ [ [Object] ] ] ]'); + arr[0][0][0] = arr; + assert.strictEqual(util.inspect(arr), '[ [ [ [Circular] ] ] ]'); +} + // Corner cases. { const x = { constructor: 42 };