Created
February 9, 2010 16:50
-
-
Save creationix/299390 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
From 1734c81a20a2b9a8dcc58f5378f98a29aedb5eff Mon Sep 17 00:00:00 2001 | |
From: Tim Caswell <tim@creationix.com> | |
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..c2489ad 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) { | |
+ // 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. | |
+ var keys = showHidden ? Object.getOwnPropertyNames(value).map(function (key) { | |
+ return '' + key; | |
+ }) : Object.keys(value); | |
+ var 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]'; | |
+ } | |
+ } | |
+ | |
+ var base, type, braces; | |
+ // 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) | |
+); | |
-- | |
1.6.5.2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment