Skip to content

Commit e33c666

Browse files
creationixry
authored andcommitted
Rewrite sys.inspect to be more reliable and handle crazy edge cases.
1 parent 3adcdfc commit e33c666

File tree

2 files changed

+131
-78
lines changed

2 files changed

+131
-78
lines changed

lib/sys.js

Lines changed: 93 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,99 @@ exports.error = function (x) {
2121
* in the best way possible given the different types.
2222
*
2323
* @param {Object} value The object to print out
24+
* @param {Boolean} showHidden Flag that shows hidden (not enumerable) properties of objects.
2425
*/
25-
exports.inspect = function (value) {
26-
return formatter(value, '', []);
26+
exports.inspect = function (obj, showHidden) {
27+
var seen = [];
28+
function format(value) {
29+
var keys, visible_keys, base, type, braces;
30+
// Primitive types cannot have properties
31+
switch (typeof value) {
32+
case 'undefined': return 'undefined';
33+
case 'string': return JSON.stringify(value);
34+
case 'number': return '' + value;
35+
case 'boolean': return '' + value;
36+
}
37+
// For some reason typeof null is "object", so special case here.
38+
if (value === null) {
39+
return 'null';
40+
}
41+
42+
// Look up the keys of the object.
43+
keys = showHidden ? Object.getOwnPropertyNames(value).map(function (key) {
44+
return '' + key;
45+
}) : Object.keys(value);
46+
visible_keys = Object.keys(value);
47+
48+
// Functions without properties can be shortcutted.
49+
if (typeof value === 'function' && keys.length === 0) {
50+
if (value instanceof RegExp) {
51+
return '' + value;
52+
} else {
53+
return '[Function]';
54+
}
55+
}
56+
57+
// Determine the object type
58+
if (value instanceof Array) {
59+
type = 'Array';
60+
braces = ["[", "]"];
61+
} else {
62+
type = 'Object';
63+
braces = ["{", "}"];
64+
}
65+
66+
// Make functions say that they are functions
67+
if (typeof value === 'function') {
68+
base = (value instanceof RegExp) ? ' ' + value : ' [Function]';
69+
} else {
70+
base = "";
71+
}
72+
73+
seen.push(value);
74+
75+
if (keys.length === 0) {
76+
return braces[0] + base + braces[1];
77+
}
78+
79+
return braces[0] + base + "\n" + (keys.map(function (key) {
80+
var name, str;
81+
if (value.__lookupGetter__) {
82+
if (value.__lookupGetter__(key)) {
83+
if (value.__lookupSetter__(key)) {
84+
str = "[Getter/Setter]";
85+
} else {
86+
str = "[Getter]";
87+
}
88+
} else {
89+
if (value.__lookupSetter__(key)) {
90+
str = "[Setter]";
91+
}
92+
}
93+
}
94+
if (visible_keys.indexOf(key) < 0) {
95+
name = "[" + key + "]";
96+
}
97+
if (!str) {
98+
if (seen.indexOf(value[key]) < 0) {
99+
str = format(value[key]);
100+
} else {
101+
str = '[Circular]';
102+
}
103+
}
104+
if (typeof name === 'undefined') {
105+
if (type === 'Array' && key.match(/^\d+$/)) {
106+
return str;
107+
}
108+
name = JSON.stringify('' + key);
109+
}
110+
111+
return name + ": " + str;
112+
}).join(",\n")).split("\n").map(function (line) {
113+
return ' ' + line;
114+
}).join('\n') + "\n" + braces[1];
115+
}
116+
return format(obj);
27117
};
28118

29119
exports.p = function (x) {
@@ -70,76 +160,4 @@ exports.exec = function (command) {
70160
*/
71161
exports.inherits = process.inherits;
72162

73-
/**
74-
* A recursive function to format an object - used by inspect.
75-
*
76-
* @param {Object} value
77-
* the value to format
78-
* @param {String} indent
79-
* the indent level of any nested objects, since they are formatted over
80-
* more than one line
81-
* @param {Array} parents
82-
* contains all objects above the current one in the heirachy, used to
83-
* prevent getting stuck in a loop on circular references
84-
*/
85-
var formatter = function(value, indent, parents) {
86-
switch(typeof(value)) {
87-
case 'string': return JSON.stringify(value);
88-
case 'number': return '' + value;
89-
case 'function': return '[Function]';
90-
case 'boolean': return '' + value;
91-
case 'undefined': return 'undefined';
92-
case 'object':
93-
if (value == null) return 'null';
94-
if (parents.indexOf(value) >= 0) return '[Circular]';
95-
parents.push(value);
96-
97-
if (value instanceof Array && Object.keys(value).length === value.length) {
98-
return formatObject(value, indent, parents, '[]', function(x, f) {
99-
return f(value[x]);
100-
});
101-
} else {
102-
return formatObject(value, indent, parents, '{}', function(x, f) {
103-
var child;
104-
if (value.__lookupGetter__(x)) {
105-
if (value.__lookupSetter__(x)) {
106-
child = "[Getter/Setter]";
107-
} else {
108-
child = "[Getter]";
109-
}
110-
} else {
111-
if (value.__lookupSetter__(x)) {
112-
child = "[Setter]";
113-
} else {
114-
child = f(value[x]);
115-
}
116-
}
117-
return f(x) + ': ' + child;
118-
});
119-
}
120-
return buffer;
121-
default:
122-
throw('inspect unimplemented for ' + typeof(value));
123-
}
124-
}
125-
126-
/**
127-
* Helper function for formatting either an array or an object, used internally by formatter
128-
*/
129-
var formatObject = function(obj, indent, parents, parenthesis, entryFormatter) {
130-
var buffer = parenthesis[0];
131-
var values = [];
132-
var x;
133-
134-
var localFormatter = function(value) {
135-
return formatter(value, indent + ' ', parents);
136-
};
137-
for (x in obj) {
138-
values.push(indent + ' ' + entryFormatter(x, localFormatter));
139-
}
140-
if (values.length > 0) {
141-
buffer += "\n" + values.join(",\n") + "\n" + indent;
142-
}
143-
buffer += parenthesis[1];
144-
return buffer;
145-
}
163+
// Object.create(null, {name: {value: "Tim", enumerable: true}})

test/mjsunit/test-sys.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ assert.equal('"hello"', inspect("hello"));
99
assert.equal("[Function]", inspect(function() {}));
1010
assert.equal('undefined', inspect(undefined));
1111
assert.equal('null', inspect(null));
12+
assert.equal('/foo(bar\\n)?/gi', inspect(/foo(bar\n)?/gi));
1213

1314
assert.equal("\"\\n\\u0001\"", inspect("\n\u0001"));
1415

@@ -23,6 +24,24 @@ assert.equal('{\n "a": [Function]\n}', inspect({a: function() {}}));
2324
assert.equal('{\n "a": 1,\n "b": 2\n}', inspect({a: 1, b: 2}));
2425
assert.equal('{\n "a": {}\n}', inspect({'a': {}}));
2526
assert.equal('{\n "a": {\n "b": 2\n }\n}', inspect({'a': {'b': 2}}));
27+
assert.equal('[\n 1,\n 2,\n 3,\n [length]: 3\n]', inspect([1,2,3], true));
28+
assert.equal("{\n \"visible\": 1\n}",
29+
inspect(Object.create({}, {visible:{value:1,enumerable:true},hidden:{value:2}}))
30+
);
31+
assert.equal("{\n [hidden]: 2,\n \"visible\": 1\n}",
32+
inspect(Object.create({}, {visible:{value:1,enumerable:true},hidden:{value:2}}), true)
33+
);
34+
35+
// Objects without prototype
36+
assert.equal(
37+
"{\n [hidden]: \"secret\",\n \"name\": \"Tim\"\n}",
38+
inspect(Object.create(null, {name: {value: "Tim", enumerable: true}, hidden: {value: "secret"}}), true)
39+
);
40+
assert.equal(
41+
"{\n \"name\": \"Tim\"\n}",
42+
inspect(Object.create(null, {name: {value: "Tim", enumerable: true}, hidden: {value: "secret"}}))
43+
);
44+
2645

2746
// Dynamic properties
2847
assert.equal(
@@ -35,12 +54,28 @@ value['a'] = value;
3554
assert.equal('{\n "a": [Circular]\n}', inspect(value));
3655
value = Object.create([]);
3756
value.push(1);
38-
assert.equal('{\n "0": 1,\n "length": 1\n}', inspect(value));
57+
assert.equal("[\n 1,\n \"length\": 1\n]", inspect(value));
3958

4059
// Array with dynamic properties
4160
value = [1,2,3];
4261
value.__defineGetter__('growingLength', function () { this.push(true); return this.length; });
4362
assert.equal(
44-
"{\n \"0\": 1,\n \"1\": 2,\n \"2\": 3,\n \"growingLength\": [Getter]\n}",
63+
"[\n 1,\n 2,\n 3,\n \"growingLength\": [Getter]\n]",
4564
inspect(value)
46-
);
65+
);
66+
67+
// Function with properties
68+
value = function () {};
69+
value.aprop = 42;
70+
assert.equal(
71+
"{ [Function]\n \"aprop\": 42\n}",
72+
inspect(value)
73+
);
74+
75+
// Regular expressions with properties
76+
value = /123/ig;
77+
value.aprop = 42;
78+
assert.equal(
79+
"{ /123/gi\n \"aprop\": 42\n}",
80+
inspect(value)
81+
);

0 commit comments

Comments
 (0)