From e33c66654ab1634fea36de13065d1bfd52fce265 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 9 Feb 2010 10:50:05 -0600 Subject: [PATCH] Rewrite sys.inspect to be more reliable and handle crazy edge cases. --- lib/sys.js | 168 ++++++++++++++++++++++++++--------------------- test/mjsunit/test-sys.js | 41 +++++++++++- 2 files changed, 131 insertions(+), 78 deletions(-) diff --git a/lib/sys.js b/lib/sys.js index 18b6359..f39a7cf 100644 --- a/lib/sys.js +++ b/lib/sys.js @@ -21,9 +21,99 @@ exports.error = function (x) { * in the best way possible given the different types. * * @param {Object} value The object to print out + * @param {Boolean} showHidden Flag that shows hidden (not enumerable) properties of objects. */ -exports.inspect = function (value) { - return formatter(value, '', []); +exports.inspect = function (obj, showHidden) { + var seen = []; + function format(value) { + var keys, visible_keys, base, type, braces; + // Primitive types cannot have properties + switch (typeof value) { + case 'undefined': return 'undefined'; + case 'string': return JSON.stringify(value); + case 'number': return '' + value; + case 'boolean': return '' + value; + } + // For some reason typeof null is "object", so special case here. + if (value === null) { + return 'null'; + } + + // Look up the keys of the object. + keys = showHidden ? Object.getOwnPropertyNames(value).map(function (key) { + return '' + key; + }) : Object.keys(value); + visible_keys = Object.keys(value); + + // Functions without properties can be shortcutted. + if (typeof value === 'function' && keys.length === 0) { + if (value instanceof RegExp) { + return '' + value; + } else { + return '[Function]'; + } + } + + // Determine the object type + if (value instanceof Array) { + type = 'Array'; + braces = ["[", "]"]; + } else { + type = 'Object'; + braces = ["{", "}"]; + } + + // Make functions say that they are functions + if (typeof value === 'function') { + base = (value instanceof RegExp) ? ' ' + value : ' [Function]'; + } else { + base = ""; + } + + seen.push(value); + + if (keys.length === 0) { + return braces[0] + base + braces[1]; + } + + return braces[0] + base + "\n" + (keys.map(function (key) { + var name, str; + if (value.__lookupGetter__) { + if (value.__lookupGetter__(key)) { + if (value.__lookupSetter__(key)) { + str = "[Getter/Setter]"; + } else { + str = "[Getter]"; + } + } else { + if (value.__lookupSetter__(key)) { + str = "[Setter]"; + } + } + } + if (visible_keys.indexOf(key) < 0) { + name = "[" + key + "]"; + } + if (!str) { + if (seen.indexOf(value[key]) < 0) { + str = format(value[key]); + } else { + str = '[Circular]'; + } + } + if (typeof name === 'undefined') { + if (type === 'Array' && key.match(/^\d+$/)) { + return str; + } + name = JSON.stringify('' + key); + } + + return name + ": " + str; + }).join(",\n")).split("\n").map(function (line) { + return ' ' + line; + }).join('\n') + "\n" + braces[1]; + } + return format(obj); }; exports.p = function (x) { @@ -70,76 +160,4 @@ exports.exec = function (command) { */ exports.inherits = process.inherits; -/** - * A recursive function to format an object - used by inspect. - * - * @param {Object} value - * the value to format - * @param {String} indent - * the indent level of any nested objects, since they are formatted over - * more than one line - * @param {Array} parents - * contains all objects above the current one in the heirachy, used to - * prevent getting stuck in a loop on circular references - */ -var formatter = function(value, indent, parents) { - switch(typeof(value)) { - case 'string': return JSON.stringify(value); - case 'number': return '' + value; - case 'function': return '[Function]'; - case 'boolean': return '' + value; - case 'undefined': return 'undefined'; - case 'object': - if (value == null) return 'null'; - if (parents.indexOf(value) >= 0) return '[Circular]'; - parents.push(value); - - if (value instanceof Array && Object.keys(value).length === value.length) { - return formatObject(value, indent, parents, '[]', function(x, f) { - return f(value[x]); - }); - } else { - return formatObject(value, indent, parents, '{}', function(x, f) { - var child; - if (value.__lookupGetter__(x)) { - if (value.__lookupSetter__(x)) { - child = "[Getter/Setter]"; - } else { - child = "[Getter]"; - } - } else { - if (value.__lookupSetter__(x)) { - child = "[Setter]"; - } else { - child = f(value[x]); - } - } - return f(x) + ': ' + child; - }); - } - return buffer; - default: - throw('inspect unimplemented for ' + typeof(value)); - } -} - -/** - * Helper function for formatting either an array or an object, used internally by formatter - */ -var formatObject = function(obj, indent, parents, parenthesis, entryFormatter) { - var buffer = parenthesis[0]; - var values = []; - var x; - - var localFormatter = function(value) { - return formatter(value, indent + ' ', parents); - }; - for (x in obj) { - values.push(indent + ' ' + entryFormatter(x, localFormatter)); - } - if (values.length > 0) { - buffer += "\n" + values.join(",\n") + "\n" + indent; - } - buffer += parenthesis[1]; - return buffer; -} +// Object.create(null, {name: {value: "Tim", enumerable: true}}) \ No newline at end of file diff --git a/test/mjsunit/test-sys.js b/test/mjsunit/test-sys.js index 005834c..6ba2157 100644 --- a/test/mjsunit/test-sys.js +++ b/test/mjsunit/test-sys.js @@ -9,6 +9,7 @@ assert.equal('"hello"', inspect("hello")); assert.equal("[Function]", inspect(function() {})); assert.equal('undefined', inspect(undefined)); assert.equal('null', inspect(null)); +assert.equal('/foo(bar\\n)?/gi', inspect(/foo(bar\n)?/gi)); assert.equal("\"\\n\\u0001\"", inspect("\n\u0001")); @@ -23,6 +24,24 @@ assert.equal('{\n "a": [Function]\n}', inspect({a: function() {}})); assert.equal('{\n "a": 1,\n "b": 2\n}', inspect({a: 1, b: 2})); assert.equal('{\n "a": {}\n}', inspect({'a': {}})); assert.equal('{\n "a": {\n "b": 2\n }\n}', inspect({'a': {'b': 2}})); +assert.equal('[\n 1,\n 2,\n 3,\n [length]: 3\n]', inspect([1,2,3], true)); +assert.equal("{\n \"visible\": 1\n}", + inspect(Object.create({}, {visible:{value:1,enumerable:true},hidden:{value:2}})) +); +assert.equal("{\n [hidden]: 2,\n \"visible\": 1\n}", + inspect(Object.create({}, {visible:{value:1,enumerable:true},hidden:{value:2}}), true) +); + +// Objects without prototype +assert.equal( + "{\n [hidden]: \"secret\",\n \"name\": \"Tim\"\n}", + inspect(Object.create(null, {name: {value: "Tim", enumerable: true}, hidden: {value: "secret"}}), true) +); +assert.equal( + "{\n \"name\": \"Tim\"\n}", + inspect(Object.create(null, {name: {value: "Tim", enumerable: true}, hidden: {value: "secret"}})) +); + // Dynamic properties assert.equal( @@ -35,12 +54,28 @@ value['a'] = value; assert.equal('{\n "a": [Circular]\n}', inspect(value)); value = Object.create([]); value.push(1); -assert.equal('{\n "0": 1,\n "length": 1\n}', inspect(value)); +assert.equal("[\n 1,\n \"length\": 1\n]", inspect(value)); // Array with dynamic properties value = [1,2,3]; value.__defineGetter__('growingLength', function () { this.push(true); return this.length; }); assert.equal( - "{\n \"0\": 1,\n \"1\": 2,\n \"2\": 3,\n \"growingLength\": [Getter]\n}", + "[\n 1,\n 2,\n 3,\n \"growingLength\": [Getter]\n]", inspect(value) -); \ No newline at end of file +); + +// Function with properties +value = function () {}; +value.aprop = 42; +assert.equal( + "{ [Function]\n \"aprop\": 42\n}", + inspect(value) +); + +// Regular expressions with properties +value = /123/ig; +value.aprop = 42; +assert.equal( + "{ /123/gi\n \"aprop\": 42\n}", + inspect(value) +); -- 2.7.4