Rewrite sys.inspect to be more reliable and handle crazy edge cases.
This commit is contained in:
parent
3adcdfc2e1
commit
e33c66654a
168
lib/sys.js
168
lib/sys.js
@ -21,9 +21,99 @@ exports.error = function (x) {
|
|||||||
* in the best way possible given the different types.
|
* in the best way possible given the different types.
|
||||||
*
|
*
|
||||||
* @param {Object} value The object to print out
|
* @param {Object} value The object to print out
|
||||||
|
* @param {Boolean} showHidden Flag that shows hidden (not enumerable) properties of objects.
|
||||||
*/
|
*/
|
||||||
exports.inspect = function (value) {
|
exports.inspect = function (obj, showHidden) {
|
||||||
return formatter(value, '', []);
|
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) {
|
exports.p = function (x) {
|
||||||
@ -70,76 +160,4 @@ exports.exec = function (command) {
|
|||||||
*/
|
*/
|
||||||
exports.inherits = process.inherits;
|
exports.inherits = process.inherits;
|
||||||
|
|
||||||
/**
|
// Object.create(null, {name: {value: "Tim", enumerable: true}})
|
||||||
* 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;
|
|
||||||
}
|
|
@ -9,6 +9,7 @@ assert.equal('"hello"', inspect("hello"));
|
|||||||
assert.equal("[Function]", inspect(function() {}));
|
assert.equal("[Function]", inspect(function() {}));
|
||||||
assert.equal('undefined', inspect(undefined));
|
assert.equal('undefined', inspect(undefined));
|
||||||
assert.equal('null', inspect(null));
|
assert.equal('null', inspect(null));
|
||||||
|
assert.equal('/foo(bar\\n)?/gi', inspect(/foo(bar\n)?/gi));
|
||||||
|
|
||||||
assert.equal("\"\\n\\u0001\"", inspect("\n\u0001"));
|
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": 1,\n "b": 2\n}', inspect({a: 1, b: 2}));
|
||||||
assert.equal('{\n "a": {}\n}', inspect({'a': {}}));
|
assert.equal('{\n "a": {}\n}', inspect({'a': {}}));
|
||||||
assert.equal('{\n "a": {\n "b": 2\n }\n}', inspect({'a': {'b': 2}}));
|
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
|
// Dynamic properties
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@ -35,12 +54,28 @@ value['a'] = value;
|
|||||||
assert.equal('{\n "a": [Circular]\n}', inspect(value));
|
assert.equal('{\n "a": [Circular]\n}', inspect(value));
|
||||||
value = Object.create([]);
|
value = Object.create([]);
|
||||||
value.push(1);
|
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
|
// Array with dynamic properties
|
||||||
value = [1,2,3];
|
value = [1,2,3];
|
||||||
value.__defineGetter__('growingLength', function () { this.push(true); return this.length; });
|
value.__defineGetter__('growingLength', function () { this.push(true); return this.length; });
|
||||||
assert.equal(
|
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)
|
inspect(value)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user