diff --git a/lib/util.js b/lib/util.js index a04c19c7353fdf..9293587d58787a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,6 +1,7 @@ 'use strict'; const uv = process.binding('uv'); +const Debug = require('vm').runInDebugContext('Debug'); const formatRegExp = /%[sdj%]/g; exports.format = function(f) { @@ -192,6 +193,14 @@ function arrayToHash(array) { } +function inspectPromise(p) { + var mirror = Debug.MakeMirror(p, true); + if (!mirror.isPromise()) + return null; + return {status: mirror.status(), value: mirror.promiseValue().value_}; +} + + function formatValue(ctx, value, recurseTimes) { // Provide a hook for user-specified inspect functions. // Check that value is an object with an inspect function on it @@ -276,14 +285,43 @@ function formatValue(ctx, value, recurseTimes) { } } - var base = '', array = false, braces = ['{', '}']; + var base = '', empty = false, braces, formatter; - // Make Array say that they are Array if (Array.isArray(value)) { - array = true; braces = ['[', ']']; + empty = value.length === 0; + formatter = formatArray; + } else if (value instanceof Set) { + braces = ['Set {', '}']; + // 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 (value instanceof Map) { + braces = ['Map {', '}']; + // Ditto. + if (ctx.showHidden) + keys.unshift('size'); + empty = value.size === 0; + formatter = formatMap; + } else { + // Only create a mirror if the object superficially looks like a Promise. + var promiseInternals = value instanceof Promise && inspectPromise(value); + if (promiseInternals) { + braces = ['Promise {', '}']; + formatter = formatPromise; + } else { + braces = ['{', '}']; + empty = true; // No other data than keys. + formatter = formatObject; + } } + empty = empty === true && keys.length === 0; + // Make functions say that they are functions if (typeof value === 'function') { var n = value.name ? ': ' + value.name : ''; @@ -323,7 +361,7 @@ function formatValue(ctx, value, recurseTimes) { base = ' ' + '[Boolean: ' + formatted + ']'; } - if (keys.length === 0 && (!array || value.length === 0)) { + if (empty === true) { return braces[0] + base + braces[1]; } @@ -337,14 +375,7 @@ function formatValue(ctx, value, recurseTimes) { ctx.seen.push(value); - var output; - if (array) { - output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); - } else { - output = keys.map(function(key) { - return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); - }); - } + var output = formatter(ctx, value, recurseTimes, visibleKeys, keys); ctx.seen.pop(); @@ -397,6 +428,13 @@ function formatError(value) { } +function formatObject(ctx, value, recurseTimes, visibleKeys, keys) { + return keys.map(function(key) { + return formatProperty(ctx, value, recurseTimes, visibleKeys, key, false); + }); +} + + function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { var output = []; for (var i = 0, l = value.length; i < l; ++i) { @@ -417,6 +455,59 @@ function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { } +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); + }); + keys.forEach(function(key) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + key, false)); + }); + 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); + }); + keys.forEach(function(key) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + key, false)); + }); + return output; +} + +function formatPromise(ctx, value, recurseTimes, visibleKeys, keys) { + var output = []; + var internals = inspectPromise(value); + if (internals.status === 'pending') { + output.push(''); + } else { + var nextRecurseTimes = recurseTimes === null ? null : recurseTimes - 1; + var str = formatValue(ctx, internals.value, nextRecurseTimes); + if (internals.status === 'rejected') { + output.push(' ' + str); + } else { + output.push(str); + } + } + keys.forEach(function(key) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + key, false)); + }); + return output; +} + + function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { var name, str, desc; desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; @@ -488,7 +579,10 @@ function reduceToSingleString(output, base, braces) { if (length > 60) { return braces[0] + - (base === '' ? '' : base + '\n ') + + // 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 ') + ' ' + diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index f4aeced4d98abc..ac4bb0e84706b9 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -235,3 +235,68 @@ if (typeof Symbol !== 'undefined') { assert.equal(util.inspect(subject, options), '[ 1, 2, 3, [length]: 3, [Symbol(symbol)]: 42 ]'); } + +// test Set +assert.equal(util.inspect(new Set), 'Set {}'); +assert.equal(util.inspect(new Set([1, 2, 3])), 'Set { 1, 2, 3 }'); +var set = new Set(['foo']); +set.bar = 42; +assert.equal(util.inspect(set, true), 'Set { \'foo\', [size]: 1, bar: 42 }'); + +// test Map +assert.equal(util.inspect(new Map), 'Map {}'); +assert.equal(util.inspect(new Map([[1, 'a'], [2, 'b'], [3, 'c']])), + 'Map { 1 => \'a\', 2 => \'b\', 3 => \'c\' }'); +var map = new Map([['foo', null]]); +map.bar = 42; +assert.equal(util.inspect(map, true), + 'Map { \'foo\' => null, [size]: 1, bar: 42 }'); + +// test Promise +assert.equal(util.inspect(Promise.resolve(3)), 'Promise { 3 }'); +assert.equal(util.inspect(Promise.reject(3)), 'Promise { 3 }'); +assert.equal(util.inspect(new Promise(function() {})), 'Promise { }'); +var promise = Promise.resolve('foo'); +promise.bar = 42; +assert.equal(util.inspect(promise), 'Promise { \'foo\', bar: 42 }'); + +// Make sure it doesn't choke on polyfills. Unlike Set/Map, there is no standard +// interface to synchronously inspect a Promise, so our techniques only work on +// a bonafide native Promise. +var oldPromise = Promise; +global.Promise = function() { this.bar = 42; }; +assert.equal(util.inspect(new Promise), '{ bar: 42 }'); +global.Promise = oldPromise; + + +// Test alignment of items in container +// Assumes that the first numeric character is the start of an item. + +function checkAlignment(container) { + var lines = util.inspect(container).split('\n'); + var pos; + lines.forEach(function(line) { + var npos = line.search(/\d/); + if (npos !== -1) { + if (pos !== undefined) + assert.equal(pos, npos, 'container items not aligned'); + pos = npos; + } + }); +} + +var big_array = []; +for (var i = 0; i < 100; i++) { + big_array.push(i); +} + +checkAlignment(big_array); +checkAlignment(function() { + var obj = {}; + big_array.forEach(function(v) { + obj[v] = null; + }); + return obj; +}()); +checkAlignment(new Set(big_array)); +checkAlignment(new Map(big_array.map(function (y) { return [y, null] })));