From a0909c7372e31034fb7dadab3407ea1fe815ff91 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sun, 14 Apr 2019 21:37:03 +0200 Subject: [PATCH] util: improve function inspection This commit contains the following changes: 1) In case a property value is a function with a name identical to the property name, the function name is suppressed to reduce redundancy. 2) Add null prototype support for functions. 3) Safely detect async and generator functions. 4) Mark anonymous functions as such instead of just leaving out the name. --- lib/internal/util/inspect.js | 37 +++++++++--- test/parallel/test-assert.js | 4 +- test/parallel/test-console-table.js | 2 +- test/parallel/test-repl.js | 2 +- test/parallel/test-util-format.js | 6 +- test/parallel/test-util-inspect-proxy.js | 4 +- test/parallel/test-util-inspect.js | 76 +++++++++++++++++++----- 7 files changed, 101 insertions(+), 30 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index fccb46085d4ead..8ffa3032683649 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -49,6 +49,8 @@ const { } = require('internal/errors'); const { + isAsyncFunction, + isGeneratorFunction, isAnyArrayBuffer, isArrayBuffer, isArgumentsObject, @@ -642,14 +644,9 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { return `${braces[0]}}`; } } else if (typeof value === 'function') { - const type = constructor || tag || 'Function'; - let name = `${type}`; - if (value.name && typeof value.name === 'string') { - name += `: ${value.name}`; - } + base = getFunctionBase(value, constructor, tag); if (keys.length === 0) - return ctx.stylize(`[${name}]`, 'special'); - base = `[${name}]`; + return ctx.stylize(base, 'special'); } else if (isRegExp(value)) { // Make RegExps say that they are RegExps base = RegExpPrototype.toString( @@ -834,6 +831,32 @@ function getBoxedBase(value, ctx, keys, constructor, tag) { return ctx.stylize(base, type.toLowerCase()); } +function getFunctionBase(value, constructor, tag) { + let type = 'Function'; + if (isAsyncFunction(value)) { + type = 'AsyncFunction'; + } else if (isGeneratorFunction(value)) { + type = 'GeneratorFunction'; + } + let base = `[${type}`; + if (constructor === null) { + base += ' (null prototype)'; + } + if (value.name === '') { + base += ' (anonymous)'; + } else { + base += `: ${value.name}`; + } + base += ']'; + if (constructor !== type && constructor !== null) { + base += ` ${constructor}`; + } + if (tag !== '' && constructor !== tag) { + base += ` [${tag}]`; + } + return base; +} + function formatError(err, constructor, tag, ctx) { // TODO(BridgeAR): Always show the error code if present. let stack = err.stack || ErrorPrototype.toString(err); diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index 62ed50f6a43fa9..fb9646035ceedc 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -289,7 +289,7 @@ testAssertionMessage(undefined, 'undefined'); testAssertionMessage(-Infinity, '-Infinity'); testAssertionMessage([1, 2, 3], '[\n+ 1,\n+ 2,\n+ 3\n+ ]'); testAssertionMessage(function f() {}, '[Function: f]'); -testAssertionMessage(function() {}, '[Function]'); +testAssertionMessage(function() {}, '[Function (anonymous)]'); testAssertionMessage(circular, '{\n+ x: [Circular],\n+ y: 1\n+ }'); testAssertionMessage({ a: undefined, b: null }, '{\n+ a: undefined,\n+ b: null\n+ }'); @@ -597,7 +597,7 @@ assert.throws( '\n' + '+ {}\n' + '- {\n' + - '- [Symbol(nodejs.util.inspect.custom)]: [Function],\n' + + '- [Symbol(nodejs.util.inspect.custom)]: [Function (anonymous)],\n' + "- loop: 'forever'\n" + '- }' }); diff --git a/test/parallel/test-console-table.js b/test/parallel/test-console-table.js index 3a4d6fefbbc8f1..98c6dd8776fda2 100644 --- a/test/parallel/test-console-table.js +++ b/test/parallel/test-console-table.js @@ -32,7 +32,7 @@ test(undefined, 'undefined\n'); test(false, 'false\n'); test('hi', 'hi\n'); test(Symbol(), 'Symbol()\n'); -test(function() {}, '[Function]\n'); +test(function() {}, '[Function (anonymous)]\n'); test([1, 2, 3], ` ┌─────────┬────────┐ diff --git a/test/parallel/test-repl.js b/test/parallel/test-repl.js index f9f5d196aa3e6b..89f53cd1869b72 100644 --- a/test/parallel/test-repl.js +++ b/test/parallel/test-repl.js @@ -308,7 +308,7 @@ const errorTests = [ // Functions should not evaluate twice (#2773) { send: 'var I = [1,2,3,function() {}]; I.pop()', - expect: '[Function]' + expect: '[Function (anonymous)]' }, // Multiline object { diff --git a/test/parallel/test-util-format.js b/test/parallel/test-util-format.js index 206b66e17c3be0..0869d53f22a82a 100644 --- a/test/parallel/test-util-format.js +++ b/test/parallel/test-util-format.js @@ -288,7 +288,7 @@ assert.strictEqual(util.format('abc%', 1), 'abc% 1'); // Additional arguments after format specifiers assert.strictEqual(util.format('%i', 1, 'number'), '1 number'); -assert.strictEqual(util.format('%i', 1, () => {}), '1 [Function]'); +assert.strictEqual(util.format('%i', 1, () => {}), '1 [Function (anonymous)]'); { const o = {}; @@ -339,8 +339,8 @@ assert.strictEqual(util.format('1', '1'), '1 1'); assert.strictEqual(util.format(1, '1'), '1 1'); assert.strictEqual(util.format('1', 1), '1 1'); assert.strictEqual(util.format(1, -0), '1 -0'); -assert.strictEqual(util.format('1', () => {}), '1 [Function]'); -assert.strictEqual(util.format(1, () => {}), '1 [Function]'); +assert.strictEqual(util.format('1', () => {}), '1 [Function (anonymous)]'); +assert.strictEqual(util.format(1, () => {}), '1 [Function (anonymous)]'); assert.strictEqual(util.format('1', "'"), "1 '"); assert.strictEqual(util.format(1, "'"), "1 '"); assert.strictEqual(util.format('1', 'number'), '1 number'); diff --git a/test/parallel/test-util-inspect-proxy.js b/test/parallel/test-util-inspect-proxy.js index c20af7450a426e..da0512eda1a8b2 100644 --- a/test/parallel/test-util-inspect-proxy.js +++ b/test/parallel/test-util-inspect-proxy.js @@ -144,7 +144,7 @@ const proxy11 = new Proxy(() => {}, { return proxy11; } }); -const expected10 = '[Function]'; -const expected11 = '[Function]'; +const expected10 = '[Function (anonymous)]'; +const expected11 = '[Function (anonymous)]'; assert.strictEqual(util.inspect(proxy10), expected10); assert.strictEqual(util.inspect(proxy11), expected11); diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 5ac270f5f84e9a..ccc0e80fbdf98e 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -33,11 +33,51 @@ assert.strictEqual(util.inspect(1), '1'); assert.strictEqual(util.inspect(false), 'false'); assert.strictEqual(util.inspect(''), "''"); assert.strictEqual(util.inspect('hello'), "'hello'"); -assert.strictEqual(util.inspect(function() {}), '[Function]'); -assert.strictEqual(util.inspect(() => {}), '[Function]'); -assert.strictEqual(util.inspect(async function() {}), '[AsyncFunction]'); -assert.strictEqual(util.inspect(async () => {}), '[AsyncFunction]'); -assert.strictEqual(util.inspect(function*() {}), '[GeneratorFunction]'); +assert.strictEqual(util.inspect(function abc() {}), '[Function: abc]'); +assert.strictEqual(util.inspect(() => {}), '[Function (anonymous)]'); +assert.strictEqual( + util.inspect(async function() {}), + '[AsyncFunction (anonymous)]' +); +assert.strictEqual(util.inspect(async () => {}), '[AsyncFunction (anonymous)]'); + +// Special function inspection. +{ + const fn = (() => function*() {})(); + assert.strictEqual( + util.inspect(fn), + '[GeneratorFunction (anonymous)]' + ); + Object.setPrototypeOf(fn, Object.getPrototypeOf(async () => {})); + assert.strictEqual( + util.inspect(fn), + '[GeneratorFunction (anonymous)] AsyncFunction' + ); + Object.defineProperty(fn, 'name', { value: 5, configurable: true }); + assert.strictEqual( + util.inspect(fn), + '[GeneratorFunction: 5] AsyncFunction' + ); + Object.defineProperty(fn, Symbol.toStringTag, { + value: 'Foobar', + configurable: true + }); + assert.strictEqual( + util.inspect({ ['5']: fn }), + "{ '5': [GeneratorFunction: 5] AsyncFunction [Foobar] }" + ); + Object.defineProperty(fn, 'name', { value: '5', configurable: true }); + Object.setPrototypeOf(fn, null); + assert.strictEqual( + util.inspect(fn), + '[GeneratorFunction (null prototype): 5] [Foobar]' + ); + assert.strictEqual( + util.inspect({ ['5']: fn }), + "{ '5': [GeneratorFunction (null prototype): 5] [Foobar] }" + ); +} + assert.strictEqual(util.inspect(undefined), 'undefined'); assert.strictEqual(util.inspect(null), 'null'); assert.strictEqual(util.inspect(/foo(bar\n)?/gi), '/foo(bar\\n)?/gi'); @@ -59,8 +99,9 @@ assert.strictEqual(util.inspect({}), '{}'); assert.strictEqual(util.inspect({ a: 1 }), '{ a: 1 }'); assert.strictEqual(util.inspect({ a: function() {} }), '{ a: [Function: a] }'); assert.strictEqual(util.inspect({ a: () => {} }), '{ a: [Function: a] }'); -assert.strictEqual(util.inspect({ a: async function() {} }), - '{ a: [AsyncFunction: a] }'); +// eslint-disable-next-line func-name-matching +assert.strictEqual(util.inspect({ a: async function abc() {} }), + '{ a: [AsyncFunction: abc] }'); assert.strictEqual(util.inspect({ a: async () => {} }), '{ a: [AsyncFunction: a] }'); assert.strictEqual(util.inspect({ a: function*() {} }), @@ -411,7 +452,10 @@ assert.strictEqual( { const value = (() => function() {})(); value.aprop = 42; - assert.strictEqual(util.inspect(value), '[Function] { aprop: 42 }'); + assert.strictEqual( + util.inspect(value), + '[Function (anonymous)] { aprop: 42 }' + ); } // Regular expressions with properties. @@ -1441,7 +1485,7 @@ util.inspect(process); out = util.inspect(o, { compact: false, breakLength: 3 }); expect = [ '{', - ' a: [Function],', + ' a: [Function (anonymous)],', ' b: [Number: 3]', '}' ].join('\n'); @@ -1450,7 +1494,7 @@ util.inspect(process); out = util.inspect(o, { compact: false, breakLength: 3, showHidden: true }); expect = [ '{', - ' a: [Function] {', + ' a: [Function (anonymous)] {', ' [length]: 0,', " [name]: ''", ' },', @@ -1765,8 +1809,8 @@ assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'"); [new Number(55), '[Number: 55]'], [Object(BigInt(55)), '[BigInt: 55n]'], [Object(Symbol('foo')), '[Symbol: Symbol(foo)]'], - [function() {}, '[Function]'], - [() => {}, '[Function]'], + [function() {}, '[Function (anonymous)]'], + [() => {}, '[Function (anonymous)]'], [[1, 2], '[ 1, 2 ]'], [[, , 5, , , , ], '[ <2 empty items>, 5, <3 empty items> ]'], [{ a: 5 }, '{ a: 5 }'], @@ -1955,10 +1999,14 @@ assert.strictEqual( let value = (function() { return function() {}; })(); Object.setPrototypeOf(value, null); Object.setPrototypeOf(obj, value); - assert.strictEqual(util.inspect(obj), '<[Function]> { a: true }'); + assert.strictEqual( + util.inspect(obj), + '<[Function (null prototype) (anonymous)]> { a: true }' + ); assert.strictEqual( util.inspect(obj, { colors: true }), - '<\u001b[36m[Function]\u001b[39m> { a: \u001b[33mtrue\u001b[39m }' + '<\u001b[36m[Function (null prototype) (anonymous)]\u001b[39m> ' + + '{ a: \u001b[33mtrue\u001b[39m }' ); obj = { a: true };