util: add Map and Set inspection support
authorChristopher Monsanto <chris@monsan.to>
Mon, 20 Apr 2015 00:29:59 +0000 (20:29 -0400)
committerChris Dickinson <christopher.s.dickinson@gmail.com>
Sun, 26 Apr 2015 02:05:18 +0000 (19:05 -0700)
PR-URL: https://github.com/iojs/io.js/pull/1471
Reviewed-By: Chris Dickinson <christopher.s.dickinson@gmail.com>
lib/util.js
test/parallel/test-util-inspect.js

index a04c19c..9293587 100644 (file)
@@ -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('<pending>');
+  } else {
+    var nextRecurseTimes = recurseTimes === null ? null : recurseTimes - 1;
+    var str = formatValue(ctx, internals.value, nextRecurseTimes);
+    if (internals.status === 'rejected') {
+      output.push('<rejected> ' + 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  ') +
            ' ' +
index f4aeced..ac4bb0e 100644 (file)
@@ -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 { <rejected> 3 }');
+assert.equal(util.inspect(new Promise(function() {})), 'Promise { <pending> }');
+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] })));