Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: allow symbol-based custom inspection methods #8174

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,32 +275,48 @@ The predefined color codes are: `white`, `grey`, `black`, `blue`, `cyan`,
Color styling uses ANSI control codes that may not be supported on all
terminals.

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

<!-- type=misc -->

Objects may also define their own `inspect(depth, opts)` function that
`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');

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

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

Custom `inspect(depth, opts)` functions typically return a string but may
return a value of any type that will be formatted accordingly by
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
const util = require('util');

const obj = { foo: 'this will not show up in the inspect() output' };
obj.inspect = function(depth) {
return { bar: 'baz' };
Expand Down Expand Up @@ -330,6 +346,14 @@ util.inspect.defaultOptions.maxArrayLength = null;
console.log(arr); // logs the full array
```

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

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

## Deprecated APIs

The following APIs have been deprecated and should no longer be used. Existing
Expand Down Expand Up @@ -807,6 +831,7 @@ similar built-in functionality through [`Object.assign()`].
[semantically incompatible]: https://github.com/nodejs/node/issues/4179
[`util.inspect()`]: #util_util_inspect_object_options
[Customizing `util.inspect` colors]: #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 @@ -500,8 +500,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 @@ -511,6 +511,7 @@ Buffer.prototype.inspect = function inspect() {
}
return '<' + this.constructor.name + ' ' + str + '>';
};
Buffer.prototype.inspect = Buffer.prototype[internalUtil.inspectSymbol];

Buffer.prototype.compare = function compare(target,
start,
Expand Down
4 changes: 4 additions & 0 deletions lib/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const kDecoratedPrivateSymbolIndex = binding['decorated_private_symbol'];
exports.getHiddenValue = binding.getHiddenValue;
exports.setHiddenValue = binding.setHiddenValue;

// 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
32 changes: 21 additions & 11 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,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 @@ -350,18 +353,25 @@ 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 the custom inspection method returned `this`, don't go into
// infinite recursion.
if (ret !== value) {
if (typeof ret !== 'string') {
ret = formatValue(ctx, ret, recurseTimes);
}
return ret;
}
}
return ret;
}

// Primitive types cannot have properties
Expand Down
66 changes: 64 additions & 2 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ 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.strictEqual(
Expand Down Expand Up @@ -482,9 +482,11 @@ assert.doesNotThrow(function() {
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.strictEqual(
util.inspect(subject, { customInspect: true }).includes('123'),
Expand Down Expand Up @@ -515,6 +517,66 @@ 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
);
}

{
// Returning `this` from a custom inspection function works.
assert.strictEqual(util.inspect({ a: 123, inspect() { return this; } }),
'{ a: 123, inspect: [Function: inspect] }');

const subject = { a: 123, [util.inspect.custom]() { return this; } };
assert.strictEqual(util.inspect(subject),
'{ a: 123 }');
}

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