From 52d4f1febfe0b52f71868a18aa47e2f95f27b734 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) Add null prototype support for functions. 2) Safely detect async and generator functions. 3) Mark anonymous functions as such instead of just leaving out the name. PR-URL: https://github.com/nodejs/node/pull/27227 Backport-PR-URL: https://github.com/nodejs/node/pull/27570 Reviewed-By: Joyee Cheung Reviewed-By: John-David Dalton Reviewed-By: Anto Aravinth Reviewed-By: Rich Trott --- 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 5a16c0fa99f558..22dec67330cd31 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 003e67b380d88d..98f728acfa4ed0 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 c0869c84b30b71..05608bd5ee6ac3 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 6e5c50ab406b83..fa0f66ecfc3fa1 100644 --- a/test/parallel/test-util-format.js +++ b/test/parallel/test-util-format.js @@ -293,7 +293,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 = {}; @@ -344,8 +344,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 5c54654a73ce58..1bab0fe0a7dff7 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]: ''", ' },', @@ -1767,8 +1811,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 }'], @@ -1957,10 +2001,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 };