From 59714cb7b3918c9e4fcd94013b778078d3710705 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 19 Aug 2016 00:26:36 +0200 Subject: [PATCH] util: allow symbol-based custom inspection methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: https://github.com/nodejs/node/issues/8071 PR-URL: https://github.com/nodejs/node/pull/8174 Reviewed-By: James M Snell Reviewed-By: Michaƫl Zasso --- doc/api/util.md | 37 ++++++++++++++++---- lib/buffer.js | 5 +-- lib/internal/util.js | 4 +++ lib/util.js | 27 ++++++++------ test/parallel/test-util-inspect.js | 56 ++++++++++++++++++++++++++++-- 5 files changed, 108 insertions(+), 21 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index ace6a7d3d0d9f7..6b6514debc820f 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -275,18 +275,19 @@ 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 -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}}`; }; @@ -294,13 +295,28 @@ 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' }; @@ -330,6 +346,14 @@ util.inspect.defaultOptions.maxArrayLength = null; console.log(arr); // logs the full array ``` +### util.inspect.custom + + +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 @@ -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 diff --git a/lib/buffer.js b/lib/buffer.js index 84f63a57df42c9..c5e5a4e58fe296 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -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) { @@ -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, diff --git a/lib/internal/util.js b/lib/internal/util.js index 055f8779b18b7a..7f93e1a2e021cb 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -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) { diff --git a/lib/util.js b/lib/util.js index ed417036e87e7d..256fafc65edfa9 100644 --- a/lib/util.js +++ b/lib/util.js @@ -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]; @@ -350,18 +353,20 @@ 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 diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 3632df86654fb0..4f0c3aebf63d12 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -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( @@ -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'), @@ -515,6 +517,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) {