Skip to content

Commit

Permalink
util: allow symbol-based custom inspection methods
Browse files Browse the repository at this point in the history
Add a `util.inspect.custom` Symbol which can be used to customize
`util.inspect()` output. Providing `obj[util.inspect.custom]`
works like providing `obj.inspect`, except that the former allows
avoiding name clashes with other `inspect()` methods.

Fixes: nodejs#8071
PR-URL: nodejs#8174
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michaël Zasso <mic.besace@gmail.com>
  • Loading branch information
addaleax committed Nov 18, 2016
1 parent 475fe96 commit 55906f9
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 32 deletions.
45 changes: 36 additions & 9 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,31 +234,49 @@ Predefined color codes are: `white`, `grey`, `black`, `blue`, `cyan`,
`green`, `magenta`, `red` and `yellow`.
There are also `bold`, `italic`, `underline` and `inverse` codes.

### Custom `inspect()` function on Objects
### Custom inspection functions on Objects

<!-- type=misc -->

Objects also may define their own `inspect(depth)` function which `util.inspect()`
will invoke and use the result of when inspecting the object:
Objects may also define their own `[util.inspect.custom](depth, opts)`
(or, equivalently `inspect(depth, opts)`) function that `util.inspect()` will
invoke and use the result of when inspecting the object:

```js
const util = require('util');

var obj = { name: 'nate' };
obj.inspect = function(depth) {
const obj = { name: 'nate' };
obj[util.inspect.custom] = function(depth) {
return `{${this.name}}`;
};

util.inspect(obj);
// "{nate}"
```

You may also return another Object entirely, and the returned String will be
formatted according to the returned Object. This is similar to how
`JSON.stringify()` works:
Custom `[util.inspect.custom](depth, opts)` functions typically return a string
but may return a value of any type that will be formatted accordingly by
`util.inspect()`.

```js
const util = require('util');

const obj = { foo: 'this will not show up in the inspect() output' };
obj[util.inspect.custom] = function(depth) {
return { bar: 'baz' };
};

util.inspect(obj);
// "{ bar: 'baz' }"
```

A custom inspection method can alternatively be provided by exposing
an `inspect(depth, opts)` method on the object:

```js
var obj = { foo: 'this will not show up in the inspect() output' };
const util = require('util');

const obj = { foo: 'this will not show up in the inspect() output' };
obj.inspect = function(depth) {
return { bar: 'baz' };
};
Expand All @@ -267,6 +285,14 @@ util.inspect(obj);
// "{ bar: 'baz' }"
```

### util.inspect.custom
<!-- YAML
added: REPLACEME
-->

A Symbol that can be used to declare custom inspect functions, see
[Custom inspection functions on Objects][].

## util.isArray(object)
<!-- YAML
added: v0.6.0
Expand Down Expand Up @@ -656,6 +682,7 @@ Deprecated predecessor of `console.log`.
[constructor]: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/constructor
[Customizing `util.inspect` colors]: #util_customizing_util_inspect_colors
[here]: #util_customizing_util_inspect_colors
[Custom inspection functions on Objects]: #util_custom_inspection_functions_on_objects
[`Error`]: errors.html#errors_class_error
[`console.log()`]: console.html#console_console_log_data
[`console.error()`]: console.html#console_console_error_data
Expand Down
5 changes: 3 additions & 2 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,8 @@ Buffer.prototype.equals = function equals(b) {
};


// Inspect
Buffer.prototype.inspect = function inspect() {
// Override how buffers are presented by util.inspect().
Buffer.prototype[internalUtil.inspectSymbol] = function inspect() {
var str = '';
var max = exports.INSPECT_MAX_BYTES;
if (this.length > 0) {
Expand All @@ -480,6 +480,7 @@ Buffer.prototype.inspect = function inspect() {
}
return '<' + this.constructor.name + ' ' + str + '>';
};
Buffer.prototype.inspect = Buffer.prototype[internalUtil.inspectSymbol];


Buffer.prototype.compare = function compare(b) {
Expand Down
4 changes: 4 additions & 0 deletions lib/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const prefix = '(node) ';

exports.getHiddenValue = binding.getHiddenValue;

// The `buffer` module uses this. Defining it here instead of in the public
// `util` module makes it accessible without having to `require('util')` there.
exports.customInspectSymbol = Symbol('util.inspect.custom');

// All the internal deprecations have to use this function only, as this will
// prepend the prefix to the actual message.
exports.deprecate = function(fn, msg) {
Expand Down
29 changes: 17 additions & 12 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ function inspect(obj, opts) {
if (ctx.colors) ctx.stylize = stylizeWithColor;
return formatValue(ctx, obj, ctx.depth);
}
exports.inspect = inspect;


// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
Expand Down Expand Up @@ -176,6 +175,10 @@ inspect.styles = {
'regexp': 'red'
};

const customInspectSymbol = internalUtil.customInspectSymbol;

exports.inspect = inspect;
exports.inspect.custom = customInspectSymbol;

function stylizeWithColor(str, styleType) {
var style = inspect.styles[styleType];
Expand Down Expand Up @@ -243,18 +246,20 @@ function inspectPromise(p) {
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
if (ctx.customInspect &&
value &&
typeof value.inspect === 'function' &&
// Filter out the util module, it's inspect function is special
value.inspect !== exports.inspect &&
// Also filter out any prototype objects using the circular check.
!(value.constructor && value.constructor.prototype === value)) {
var ret = value.inspect(recurseTimes, ctx);
if (typeof ret !== 'string') {
ret = formatValue(ctx, ret, recurseTimes);
if (ctx.customInspect && value) {
const maybeCustomInspect = value[customInspectSymbol] || value.inspect;

if (typeof maybeCustomInspect === 'function' &&
// Filter out the util module, its inspect function is special
maybeCustomInspect !== exports.inspect &&
// Also filter out any prototype objects using the circular check.
!(value.constructor && value.constructor.prototype === value)) {
let ret = maybeCustomInspect.call(value, recurseTimes, ctx);
if (typeof ret !== 'string') {
ret = formatValue(ctx, ret, recurseTimes);
}
return ret;
}
return ret;
}

// Primitive types cannot have properties
Expand Down
91 changes: 82 additions & 9 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,19 +279,42 @@ assert.doesNotThrow(function() {

// new API, accepts an "options" object
{
let subject = { foo: 'bar', hello: 31, a: { b: { c: { d: 0 } } } };
const subject = { foo: 'bar', hello: 31, a: { b: { c: { d: 0 } } } };
Object.defineProperty(subject, 'hidden', { enumerable: false, value: null });

assert(util.inspect(subject, { showHidden: false }).indexOf('hidden') === -1);
assert(util.inspect(subject, { showHidden: true }).indexOf('hidden') !== -1);
assert(util.inspect(subject, { colors: false }).indexOf('\u001b[32m') === -1);
assert(util.inspect(subject, { colors: true }).indexOf('\u001b[32m') !== -1);
assert(util.inspect(subject, { depth: 2 }).indexOf('c: [Object]') !== -1);
assert(util.inspect(subject, { depth: 0 }).indexOf('a: [Object]') !== -1);
assert(util.inspect(subject, { depth: null }).indexOf('{ d: 0 }') !== -1);
assert.strictEqual(
util.inspect(subject, { showHidden: false }).includes('hidden'),
false
);
assert.strictEqual(
util.inspect(subject, { showHidden: true }).includes('hidden'),
true
);
assert.strictEqual(
util.inspect(subject, { colors: false }).includes('\u001b[32m'),
false
);
assert.strictEqual(
util.inspect(subject, { colors: true }).includes('\u001b[32m'),
true
);
assert.strictEqual(
util.inspect(subject, { depth: 2 }).includes('c: [Object]'),
true
);
assert.strictEqual(
util.inspect(subject, { depth: 0 }).includes('a: [Object]'),
true
);
assert.strictEqual(
util.inspect(subject, { depth: null }).includes('{ d: 0 }'),
true
);
}

{
// "customInspect" option can enable/disable calling inspect() on objects
subject = { inspect: function() { return 123; } };
const subject = { inspect: function() { return 123; } };

assert(util.inspect(subject,
{ customInspect: true }).indexOf('123') !== -1);
Expand All @@ -314,6 +337,56 @@ assert.doesNotThrow(function() {
util.inspect(subject, { customInspectOptions: true });
}

{
// "customInspect" option can enable/disable calling [util.inspect.custom]()
const subject = { [util.inspect.custom]: function() { return 123; } };

assert.strictEqual(
util.inspect(subject, { customInspect: true }).includes('123'),
true
);
assert.strictEqual(
util.inspect(subject, { customInspect: false }).includes('123'),
false
);

// a custom [util.inspect.custom]() should be able to return other Objects
subject[util.inspect.custom] = function() { return { foo: 'bar' }; };

assert.strictEqual(util.inspect(subject), '{ foo: \'bar\' }');

subject[util.inspect.custom] = function(depth, opts) {
assert.strictEqual(opts.customInspectOptions, true);
};

util.inspect(subject, { customInspectOptions: true });
}

{
// [util.inspect.custom] takes precedence over inspect
const subject = {
[util.inspect.custom]() { return 123; },
inspect() { return 456; }
};

assert.strictEqual(
util.inspect(subject, { customInspect: true }).includes('123'),
true
);
assert.strictEqual(
util.inspect(subject, { customInspect: false }).includes('123'),
false
);
assert.strictEqual(
util.inspect(subject, { customInspect: true }).includes('456'),
false
);
assert.strictEqual(
util.inspect(subject, { customInspect: false }).includes('456'),
false
);
}

// util.inspect with "colors" option should produce as many lines as without it
function test_lines(input) {
var count_lines = function(str) {
Expand Down

0 comments on commit 55906f9

Please sign in to comment.