From 8f43f84803858061a33107e15e92907fdf70f2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sat, 3 Mar 2018 10:48:45 +0100 Subject: [PATCH 1/7] sync util.debuglog with Node 9 --- test/node/debug.js | 74 ++++++++++++++++++++++++++++------------------ util.js | 15 +++++++--- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/test/node/debug.js b/test/node/debug.js index ef5f69f..fa8b49c 100644 --- a/test/node/debug.js +++ b/test/node/debug.js @@ -19,68 +19,86 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +'use strict'; var assert = require('assert'); -var util = require('../../'); -if (process.argv[2] === 'child') - child(); +var modeArgv = process.argv[2] +var sectionArgv = process.argv[3] + +if (modeArgv === 'child') + child(sectionArgv); else parent(); function parent() { - test('foo,tud,bar', true); - test('foo,tud', true); - test('tud,bar', true); - test('tud', true); - test('foo,bar', false); - test('', false); + test('foo,tud,bar', true, 'tud'); + test('foo,tud', true, 'tud'); + test('tud,bar', true, 'tud'); + test('tud', true, 'tud'); + test('foo,bar', false, 'tud'); + test('', false, 'tud'); + + test('###', true, '###'); + test('hi:)', true, 'hi:)'); + test('f$oo', true, 'f$oo'); + test('f$oo', false, 'f.oo'); + test('no-bar-at-all', false, 'bar'); + + test('test-abc', true, 'test-abc'); + test('test-a', false, 'test-abc'); + test('test-*', true, 'test-abc'); + test('test-*c', true, 'test-abc'); + test('test-*abc', true, 'test-abc'); + test('abc-test', true, 'abc-test'); + test('a*-test', true, 'abc-test'); + test('*-test', true, 'abc-test'); } -function test(environ, shouldWrite) { +function test(environ, shouldWrite, section) { var expectErr = ''; - if (shouldWrite) { - expectErr = 'TUD %PID%: this { is: \'a\' } /debugging/\n' + - 'TUD %PID%: number=1234 string=asdf obj={"foo":"bar"}\n'; - } var expectOut = 'ok\n'; - var didTest = false; var spawn = require('child_process').spawn; - var child = spawn(process.execPath, [__filename, 'child'], { - env: { NODE_DEBUG: environ } + var child = spawn(process.execPath, [__filename, 'child', section], { + env: Object.assign(process.env, { NODE_DEBUG: environ }) }); - expectErr = expectErr.split('%PID%').join(child.pid); + if (shouldWrite) { + expectErr = + section.toUpperCase() + ' ' + child.pid + ': this { is: \'a\' } /debugging/\n' + + section.toUpperCase() + ' ' + child.pid + ': num=1 str=a obj={"foo":"bar"}\n'; + } var err = ''; child.stderr.setEncoding('utf8'); - child.stderr.on('data', function(c) { + child.stderr.on('data', function (c) { err += c; }); var out = ''; child.stdout.setEncoding('utf8'); - child.stdout.on('data', function(c) { + child.stdout.on('data', function (c) { out += c; }); - child.on('close', function(c) { + var didTest = false; + child.on('close', function (c) { assert(!c); - assert.equal(err, expectErr); - assert.equal(out, expectOut); + assert.strictEqual(err, expectErr); + assert.strictEqual(out, expectOut); didTest = true; - console.log('ok %j %j', environ, shouldWrite); }); - process.on('exit', function() { + process.on('exit', function () { assert(didTest); }); } -function child() { - var debug = util.debuglog('tud'); +function child(section) { + var util = require('../../util'); + var debug = util.debuglog(section); debug('this', { is: 'a' }, /debugging/); - debug('number=%d string=%s obj=%j', 1234, 'asdf', { foo: 'bar' }); + debug('num=%d str=%s obj=%j', 1, 'a', { foo: 'bar' }); console.log('ok'); } diff --git a/util.js b/util.js index e0ea321..44a5f72 100644 --- a/util.js +++ b/util.js @@ -94,13 +94,20 @@ exports.deprecate = function(fn, msg) { var debugs = {}; -var debugEnviron; +var debugEnvRegex = /^$/; + +if (process.env.NODE_DEBUG) { + var debugEnv = process.env.NODE_DEBUG; + debugEnv = debugEnv.replace(/[|\\{}()[\]^$+?.]/g, '\\$&') + .replace(/\*/g, '.*') + .replace(/,/g, '$|^') + .toUpperCase(); + debugEnvRegex = new RegExp('^' + debugEnv + '$', 'i'); +} exports.debuglog = function(set) { - if (isUndefined(debugEnviron)) - debugEnviron = process.env.NODE_DEBUG || ''; set = set.toUpperCase(); if (!debugs[set]) { - if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) { + if (debugEnvRegex.test(set)) { var pid = process.pid; debugs[set] = function() { var msg = exports.format.apply(exports, arguments); From f1709ddd018a122d1e47ad16e46ee904cc862ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sat, 3 Mar 2018 11:18:06 +0100 Subject: [PATCH 2/7] Add promisify --- test/node/promisify.js | 224 +++++++++++++++++++++++++++++++++++++++++ util.js | 58 +++++++++++ 2 files changed, 282 insertions(+) create mode 100644 test/node/promisify.js diff --git a/test/node/promisify.js b/test/node/promisify.js new file mode 100644 index 0000000..d1106ba --- /dev/null +++ b/test/node/promisify.js @@ -0,0 +1,224 @@ +'use strict'; +var assert = require('assert'); +var fs = require('fs'); +var vm = require('vm'); +var promisify = require('../../util').promisify; + +var mustCalls = []; +var common = { + expectsError: function (fn, props) { + try { fn(); } + catch (err) { + if (props.type) assert.equal(err.constructor, props.type); + if (props.message) assert.equal(err.message, props.message); + return; + } + assert.fail('expected error'); + }, + mustCall: function (fn) { + function mustCall() { + mustCall.called = true + return fn.apply(this, arguments); + } + + mustCalls.push(mustCall); + return mustCall; + } +}; + +var stat = promisify(fs.stat); + +{ + var promise = stat(__filename); + assert(promise instanceof Promise); + promise.then(common.mustCall(function (value) { + assert.deepStrictEqual(value, fs.statSync(__filename)); + })); +} + +{ + var promise = stat('/dontexist'); + promise.catch(common.mustCall(function (error) { + assert(error.message.indexOf('ENOENT: no such file or directory, stat') !== -1); + })); +} + +{ + function fn() {} + function promisifedFn() {} + fn[promisify.custom] = promisifedFn; + assert.strictEqual(promisify(fn), promisifedFn); + assert.strictEqual(promisify(promisify(fn)), promisifedFn); +} + +{ + function fn() {} + fn[promisify.custom] = 42; + common.expectsError( + function () { promisify(fn); }, + { code: 'ERR_INVALID_ARG_TYPE', type: TypeError } + ); +} + +// promisify args test disabled, it is an internal core API that is +// not used anywhere anymore and this package does not implement it. +if (false) { + var firstValue = 5; + var secondValue = 17; + + function fn(callback) { + callback(null, firstValue, secondValue); + } + + fn[customPromisifyArgs] = ['first', 'second']; + + promisify(fn)().then(common.mustCall(function (obj) { + assert.deepStrictEqual(obj, { first: firstValue, second: secondValue }); + })); +} + +{ + var fn = vm.runInNewContext('(function() {})'); + assert.notStrictEqual(Object.getPrototypeOf(promisify(fn)), + Function.prototype); +} + +{ + function fn(callback) { + callback(null, 'foo', 'bar'); + } + promisify(fn)().then(common.mustCall(function (value) { + assert.deepStrictEqual(value, 'foo'); + })); +} + +{ + function fn(callback) { + callback(null); + } + promisify(fn)().then(common.mustCall(function (value) { + assert.strictEqual(value, undefined); + })); +} + +{ + function fn(callback) { + callback(); + } + promisify(fn)().then(common.mustCall(function (value) { + assert.strictEqual(value, undefined); + })); +} + +{ + function fn(err, val, callback) { + callback(err, val); + } + promisify(fn)(null, 42).then(common.mustCall(function (value) { + assert.strictEqual(value, 42); + })); +} + +{ + function fn(err, val, callback) { + callback(err, val); + } + promisify(fn)(new Error('oops'), null).catch(common.mustCall(function (err) { + assert.strictEqual(err.message, 'oops'); + })); +} + +{ + function fn(err, val, callback) { + callback(err, val); + } + + + Promise.resolve() + .then(function () { promisify(fn)(null, 42); }) + .then(function (value) { + assert.strictEqual(value, 42); + }); +} + +{ + var o = {}; + var fn = promisify(function(cb) { + + cb(null, this === o); + }); + + o.fn = fn; + + o.fn().then(common.mustCall(function(val) { + assert(val); + })); +} + +(function () { + var err = new Error('Should not have called the callback with the error.'); + var stack = err.stack; + + var fn = promisify(function(cb) { + cb(null); + cb(err); + }); + + Promise.resolve() + .then(function () { return fn(); }) + .then(function () { return Promise.resolve(); }) + .then(function () { + assert.strictEqual(stack, err.stack); + }); +})(); + +{ + function c() { } + var a = promisify(function() { }); + var b = promisify(a); + assert.notStrictEqual(c, a); + assert.strictEqual(a, b); +} + +{ + var errToThrow; + var thrower = promisify(function(a, b, c, cb) { + errToThrow = new Error(); + throw errToThrow; + }); + thrower(1, 2, 3) + .then(assert.fail) + .then(assert.fail, function (e) { assert.strictEqual(e, errToThrow); }); +} + +{ + var err = new Error(); + + var a = promisify(function (cb) { cb(err) })(); + var b = promisify(function () { throw err; })(); + + Promise.all([ + a.then(assert.fail, function(e) { + assert.strictEqual(err, e); + }), + b.then(assert.fail, function(e) { + assert.strictEqual(err, e); + }) + ]); +} + +[undefined, null, true, 0, 'str', {}, [], Symbol()].forEach(function (input) { + common.expectsError( + function () { promisify(input); }, + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "original" argument must be of type Function' + }); +}); + +process.on('exit', function () { + mustCalls.forEach(function (mc) { + assert(mc.called); + }); +}); diff --git a/util.js b/util.js index 44a5f72..6569b2b 100644 --- a/util.js +++ b/util.js @@ -591,3 +591,61 @@ exports._extend = function(origin, add) { function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } + +var kCustomPromisifiedSymbol = typeof Symbol !== 'undefined' ? Symbol('util.promisify.custom') : undefined; + +exports.promisify = function promisify(original) { + if (typeof original !== 'function') + throw new TypeError('The "original" argument must be of type Function'); + + if (kCustomPromisifiedSymbol && original[kCustomPromisifiedSymbol]) { + var fn = original[kCustomPromisifiedSymbol]; + if (typeof fn !== 'function') { + throw new TypeError('The "util.promisify.custom" argument must be of type Function'); + } + Object.defineProperty(fn, kCustomPromisifiedSymbol, { + value: fn, enumerable: false, writable: false, configurable: true + }); + return fn; + } + + function fn() { + var promiseResolve, promiseReject; + var promise = new Promise(function (resolve, reject) { + promiseResolve = resolve; + promiseReject = reject; + }); + + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); + } + args.push(function (err, value) { + if (err) { + promiseReject(err); + } else { + promiseResolve(value); + } + }); + + try { + original.apply(this, args); + } catch (err) { + promiseReject(err); + } + + return promise; + } + + Object.setPrototypeOf(fn, Object.getPrototypeOf(original)); + + if (kCustomPromisifiedSymbol) Object.defineProperty(fn, kCustomPromisifiedSymbol, { + value: fn, enumerable: false, writable: false, configurable: true + }); + return Object.defineProperties( + fn, + Object.getOwnPropertyDescriptors(original) + ); +} + +exports.promisify.custom = kCustomPromisifiedSymbol From abd92285299662dfe7981fe26fc0d57d71f12377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sat, 3 Mar 2018 11:33:13 +0100 Subject: [PATCH 3/7] Export TextEncoder/TextDecoder --- util.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/util.js b/util.js index 6569b2b..ca69d82 100644 --- a/util.js +++ b/util.js @@ -649,3 +649,7 @@ exports.promisify = function promisify(original) { } exports.promisify.custom = kCustomPromisifiedSymbol + +// WHATWG TextEncoder / TextDecoder +exports.TextEncoder = typeof TextEncoder !== 'undefined' ? TextEncoder : undefined +exports.TextDecoder = typeof TextDecoder !== 'undefined' ? TextDecoder : undefined From 3b26cc34ef31c970a7f693999d2b0dfd4ed2cb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 7 Mar 2018 09:30:59 +0100 Subject: [PATCH 4/7] Test fixes --- test/node/debug.js | 3 ++- test/node/promisify.js | 54 +++++++++++++++++++++++------------------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/test/node/debug.js b/test/node/debug.js index fa8b49c..9f296a6 100644 --- a/test/node/debug.js +++ b/test/node/debug.js @@ -59,8 +59,9 @@ function test(environ, shouldWrite, section) { var expectOut = 'ok\n'; var spawn = require('child_process').spawn; + process.env.NODE_DEBUG = environ; var child = spawn(process.execPath, [__filename, 'child', section], { - env: Object.assign(process.env, { NODE_DEBUG: environ }) + env: process.env }); if (shouldWrite) { diff --git a/test/node/promisify.js b/test/node/promisify.js index d1106ba..d9c1386 100644 --- a/test/node/promisify.js +++ b/test/node/promisify.js @@ -1,9 +1,13 @@ -'use strict'; var assert = require('assert'); var fs = require('fs'); var vm = require('vm'); var promisify = require('../../util').promisify; +if (typeof Promise === 'undefined') { + console.log('no global Promise found, skipping promisify tests'); + return; +} + var mustCalls = []; var common = { expectsError: function (fn, props) { @@ -52,10 +56,10 @@ var stat = promisify(fs.stat); } { - function fn() {} - fn[promisify.custom] = 42; + function fn2() {} + fn2[promisify.custom] = 42; common.expectsError( - function () { promisify(fn); }, + function () { promisify(fn2); }, { code: 'ERR_INVALID_ARG_TYPE', type: TypeError } ); } @@ -66,76 +70,76 @@ if (false) { var firstValue = 5; var secondValue = 17; - function fn(callback) { + function fn3(callback) { callback(null, firstValue, secondValue); } - fn[customPromisifyArgs] = ['first', 'second']; + fn3[customPromisifyArgs] = ['first', 'second']; - promisify(fn)().then(common.mustCall(function (obj) { + promisify(fn3)().then(common.mustCall(function (obj) { assert.deepStrictEqual(obj, { first: firstValue, second: secondValue }); })); } { - var fn = vm.runInNewContext('(function() {})'); - assert.notStrictEqual(Object.getPrototypeOf(promisify(fn)), + var fn4 = vm.runInNewContext('(function() {})'); + assert.notStrictEqual(Object.getPrototypeOf(promisify(fn4)), Function.prototype); } { - function fn(callback) { + function fn5(callback) { callback(null, 'foo', 'bar'); } - promisify(fn)().then(common.mustCall(function (value) { + promisify(fn5)().then(common.mustCall(function (value) { assert.deepStrictEqual(value, 'foo'); })); } { - function fn(callback) { + function fn6(callback) { callback(null); } - promisify(fn)().then(common.mustCall(function (value) { + promisify(fn6)().then(common.mustCall(function (value) { assert.strictEqual(value, undefined); })); } { - function fn(callback) { + function fn7(callback) { callback(); } - promisify(fn)().then(common.mustCall(function (value) { + promisify(fn7)().then(common.mustCall(function (value) { assert.strictEqual(value, undefined); })); } { - function fn(err, val, callback) { + function fn8(err, val, callback) { callback(err, val); } - promisify(fn)(null, 42).then(common.mustCall(function (value) { + promisify(fn8)(null, 42).then(common.mustCall(function (value) { assert.strictEqual(value, 42); })); } { - function fn(err, val, callback) { + function fn9(err, val, callback) { callback(err, val); } - promisify(fn)(new Error('oops'), null).catch(common.mustCall(function (err) { + promisify(fn9)(new Error('oops'), null).catch(common.mustCall(function (err) { assert.strictEqual(err.message, 'oops'); })); } { - function fn(err, val, callback) { + function fn9(err, val, callback) { callback(err, val); } Promise.resolve() - .then(function () { promisify(fn)(null, 42); }) + .then(function () { return promisify(fn9)(null, 42); }) .then(function (value) { assert.strictEqual(value, 42); }); @@ -143,12 +147,12 @@ if (false) { { var o = {}; - var fn = promisify(function(cb) { + var fn10 = promisify(function(cb) { cb(null, this === o); }); - o.fn = fn; + o.fn = fn10; o.fn().then(common.mustCall(function(val) { assert(val); @@ -159,13 +163,13 @@ if (false) { var err = new Error('Should not have called the callback with the error.'); var stack = err.stack; - var fn = promisify(function(cb) { + var fn11 = promisify(function(cb) { cb(null); cb(err); }); Promise.resolve() - .then(function () { return fn(); }) + .then(function () { return fn11(); }) .then(function () { return Promise.resolve(); }) .then(function () { assert.strictEqual(stack, err.stack); From 15e1617a615f764e58407d530ffe993f9e7bf42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 7 Mar 2018 09:42:06 +0100 Subject: [PATCH 5/7] Update test matrix --- .travis.yml | 12 ++++++++++-- package.json | 6 ++++-- test/node/index.js | 7 +++++++ 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 test/node/index.js diff --git a/.travis.yml b/.travis.yml index ded625c..54862c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,15 @@ language: node_js node_js: -- '0.8' -- '0.10' + - 'stable' + - '8' + - '6' + - '4' + - '0.12' + - '0.10' +script: + - 'npm test' + # Run browser tests on one node version. + - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && [[ $(node -v) =~ ^v8.*$ ]] && npm run test:browsers' env: global: - secure: AdUubswCR68/eGD+WWjwTHgFbelwQGnNo81j1IOaUxKw+zgFPzSnFEEtDw7z98pWgg7p9DpCnyzzSnSllP40wq6AG19OwyUJjSLoZK57fp+r8zwTQwWiSqUgMu2YSMmKJPIO/aoSGpRQXT+L1nRrHoUJXgFodyIZgz40qzJeZjc= diff --git a/package.json b/package.json index d1206f7..12ebe74 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,16 @@ "support" ], "scripts": { - "test": "node test/node/*.js && zuul test/browser/*.js" + "test": "node test/node/index.js", + "test:browsers": "zuul test/browser/*.js" }, "dependencies": { "inherits": "2.0.1" }, "license": "MIT", "devDependencies": { - "zuul": "~1.0.9" + "tape": "^4.9.0", + "zuul": "^1.0.10" }, "browser": { "./support/isBuffer.js": "./support/isBufferBrowser.js" diff --git a/test/node/index.js b/test/node/index.js new file mode 100644 index 0000000..0403b3f --- /dev/null +++ b/test/node/index.js @@ -0,0 +1,7 @@ +var spawn = require('child_process').spawnSync; + +spawn(process.argv[0], [ require.resolve('./debug') ], { stdio: 'inherit' }); +spawn(process.argv[0], [ require.resolve('./format') ], { stdio: 'inherit' }); +spawn(process.argv[0], [ require.resolve('./inspect') ], { stdio: 'inherit' }); +spawn(process.argv[0], [ require.resolve('./log') ], { stdio: 'inherit' }); +spawn(process.argv[0], [ require.resolve('./promisify') ], { stdio: 'inherit' }); From db44a7159a115f36d91ce3de412d74ce45902ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 7 Mar 2018 09:51:50 +0100 Subject: [PATCH 6/7] Fix log(function(){}) test --- test/node/log.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/node/log.js b/test/node/log.js index 6bd96d1..7507b2d 100644 --- a/test/node/log.js +++ b/test/node/log.js @@ -33,13 +33,15 @@ global.process.stdout.write = function(string) { }; console._stderr = process.stdout; +var propFnsAreNamed = { a: function () {} }.a.name === 'a'; + var tests = [ {input: 'foo', output: 'foo'}, {input: undefined, output: 'undefined'}, {input: null, output: 'null'}, {input: false, output: 'false'}, {input: 42, output: '42'}, - {input: function(){}, output: '[Function]'}, + {input: function(){}, output: propFnsAreNamed ? '[Function: input]' : '[Function]'}, {input: parseInt('not a number', 10), output: 'NaN'}, {input: {answer: 42}, output: '{ answer: 42 }'}, {input: [1,2,3], output: '[ 1, 2, 3 ]'} From 5c6a1faa28a923435f45f4873e3a3fcc5142b454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 7 Mar 2018 10:50:33 +0100 Subject: [PATCH 7/7] Match Node 9 util.inspect() --- internal/util.js | 25 ++ package.json | 3 +- test/node/inspect.js | 470 ++++++++++++++++++++++++++++-- util.js | 669 +++++++++++++++++++++++++++++++------------ 4 files changed, 958 insertions(+), 209 deletions(-) create mode 100644 internal/util.js diff --git a/internal/util.js b/internal/util.js new file mode 100644 index 0000000..a893f82 --- /dev/null +++ b/internal/util.js @@ -0,0 +1,25 @@ +var colorRegExp = /\u001b\[\d\d?m/g; + +function removeColors(str) { + return str.replace(colorRegExp, ''); +} + +function getConstructorOf(obj) { + while (obj) { + var descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); + if (descriptor !== undefined && + typeof descriptor.value === 'function' && + descriptor.value.name !== '') { + return descriptor.value; + } + + obj = Object.getPrototypeOf(obj); + } + + return null; +} + +module.exports = { + removeColors: removeColors, + getConstructorOf: getConstructorOf +}; diff --git a/package.json b/package.json index 12ebe74..4d07dc7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "test:browsers": "zuul test/browser/*.js" }, "dependencies": { - "inherits": "2.0.1" + "inherits": "2.0.1", + "is-typed-array": "^1.0.4" }, "license": "MIT", "devDependencies": { diff --git a/test/node/inspect.js b/test/node/inspect.js index f766d11..34a3769 100644 --- a/test/node/inspect.js +++ b/test/node/inspect.js @@ -23,8 +23,380 @@ var assert = require('assert'); +var vm = require('vm'); var util = require('../../'); +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(undefined), 'undefined'); +assert.strictEqual(util.inspect(null), 'null'); +assert.strictEqual(util.inspect(/foo(bar\n)?/gi), '/foo(bar\\n)?/gi'); +assert.strictEqual( + util.inspect(new Date('Sun, 14 Feb 2010 11:48:40 GMT')), + new Date('2010-02-14T12:48:40+01:00').toISOString() +); +assert.strictEqual(util.inspect(new Date('')), (new Date('')).toString()); +assert.strictEqual(util.inspect('\n\u0001'), "'\\n\\u0001'"); +assert.strictEqual( + util.inspect(`${Array(75).fill(1)}'\n\u001d\n\u0003`), + `'${Array(75).fill(1)}\\'\\n\\u001d\\n\\u0003'` +); +assert.strictEqual(util.inspect([]), '[]'); +assert.strictEqual(util.inspect(Object.create([])), 'Array {}'); +assert.strictEqual(util.inspect([1, 2]), '[ 1, 2 ]'); +assert.strictEqual(util.inspect([1, [2, 3]]), '[ 1, [ 2, 3 ] ]'); +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] }'); +assert.strictEqual(util.inspect({ a: async () => {} }), + '{ a: [AsyncFunction: a] }'); +assert.strictEqual(util.inspect({ a: function*() {} }), + '{ a: [GeneratorFunction: a] }'); +assert.strictEqual(util.inspect({ a: 1, b: 2 }), '{ a: 1, b: 2 }'); +assert.strictEqual(util.inspect({ 'a': {} }), '{ a: {} }'); +assert.strictEqual(util.inspect({ 'a': { 'b': 2 } }), '{ a: { b: 2 } }'); +assert.strictEqual(util.inspect({ 'a': { 'b': { 'c': { 'd': 2 } } } }), + '{ a: { b: { c: [Object] } } }'); +assert.strictEqual( + util.inspect({ 'a': { 'b': { 'c': { 'd': 2 } } } }, false, null), + '{ a: { b: { c: { d: 2 } } } }'); +assert.strictEqual(util.inspect([1, 2, 3], true), '[ 1, 2, 3, [length]: 3 ]'); +assert.strictEqual(util.inspect({ 'a': { 'b': { 'c': 2 } } }, false, 0), + '{ a: [Object] }'); +assert.strictEqual(util.inspect({ 'a': { 'b': { 'c': 2 } } }, false, 1), + '{ a: { b: [Object] } }'); +assert.strictEqual(util.inspect({ 'a': { 'b': ['c'] } }, false, 1), + '{ a: { b: [Array] } }'); +assert.strictEqual(util.inspect(new Uint8Array(0)), 'Uint8Array [ ]'); +assert.strictEqual( + util.inspect( + Object.create( + {}, + { visible: { value: 1, enumerable: true }, hidden: { value: 2 } } + ) + ), + '{ visible: 1 }' +); +assert.strictEqual( + util.inspect( + Object.assign(new String('hello'), { [Symbol('foo')]: 123 }), + { showHidden: true } + ), + '{ [String: \'hello\'] [length]: 5, [Symbol(foo)]: 123 }' +); + +{ + const regexp = /regexp/; + regexp.aprop = 42; + assert.strictEqual(util.inspect({ a: regexp }, false, 0), '{ a: /regexp/ }'); +} + +assert(/Object/.test( + util.inspect({ a: { a: { a: { a: {} } } } }, undefined, undefined, true) +)); +assert(!/Object/.test( + util.inspect({ a: { a: { a: { a: {} } } } }, undefined, null, true) +)); + +for (const showHidden of [true, false]) { + const ab = new ArrayBuffer(4); + const dv = new DataView(ab, 1, 2); + assert.strictEqual( + util.inspect(ab, showHidden), + 'ArrayBuffer { byteLength: 4 }' + ); + assert.strictEqual(util.inspect(new DataView(ab, 1, 2), showHidden), + 'DataView {\n' + + ' byteLength: 2,\n' + + ' byteOffset: 1,\n' + + ' buffer: ArrayBuffer { byteLength: 4 } }'); + assert.strictEqual( + util.inspect(ab, showHidden), + 'ArrayBuffer { byteLength: 4 }' + ); + assert.strictEqual(util.inspect(dv, showHidden), + 'DataView {\n' + + ' byteLength: 2,\n' + + ' byteOffset: 1,\n' + + ' buffer: ArrayBuffer { byteLength: 4 } }'); + ab.x = 42; + dv.y = 1337; + assert.strictEqual(util.inspect(ab, showHidden), + 'ArrayBuffer { byteLength: 4, x: 42 }'); + assert.strictEqual(util.inspect(dv, showHidden), + 'DataView {\n' + + ' byteLength: 2,\n' + + ' byteOffset: 1,\n' + + ' buffer: ArrayBuffer { byteLength: 4, x: 42 },\n' + + ' y: 1337 }'); +} + +// Now do the same checks but from a different context +for (const showHidden of [true, false]) { + const ab = vm.runInNewContext('new ArrayBuffer(4)'); + const dv = vm.runInNewContext('new DataView(ab, 1, 2)', { ab }); + assert.strictEqual( + util.inspect(ab, showHidden), + 'ArrayBuffer { byteLength: 4 }' + ); + assert.strictEqual(util.inspect(new DataView(ab, 1, 2), showHidden), + 'DataView {\n' + + ' byteLength: 2,\n' + + ' byteOffset: 1,\n' + + ' buffer: ArrayBuffer { byteLength: 4 } }'); + assert.strictEqual( + util.inspect(ab, showHidden), + 'ArrayBuffer { byteLength: 4 }' + ); + assert.strictEqual(util.inspect(dv, showHidden), + 'DataView {\n' + + ' byteLength: 2,\n' + + ' byteOffset: 1,\n' + + ' buffer: ArrayBuffer { byteLength: 4 } }'); + ab.x = 42; + dv.y = 1337; + assert.strictEqual(util.inspect(ab, showHidden), + 'ArrayBuffer { byteLength: 4, x: 42 }'); + assert.strictEqual(util.inspect(dv, showHidden), + 'DataView {\n' + + ' byteLength: 2,\n' + + ' byteOffset: 1,\n' + + ' buffer: ArrayBuffer { byteLength: 4, x: 42 },\n' + + ' y: 1337 }'); +} + + +[ Float32Array, + Float64Array, + Int16Array, + Int32Array, + Int8Array, + Uint16Array, + Uint32Array, + Uint8Array, + Uint8ClampedArray ].forEach((constructor) => { + const length = 2; + const byteLength = length * constructor.BYTES_PER_ELEMENT; + const array = new constructor(new ArrayBuffer(byteLength), 0, length); + array[0] = 65; + array[1] = 97; + assert.strictEqual( + util.inspect(array, true), + `${constructor.name} [\n` + + ' 65,\n' + + ' 97,\n' + + ` [BYTES_PER_ELEMENT]: ${constructor.BYTES_PER_ELEMENT},\n` + + ` [length]: ${length},\n` + + ` [byteLength]: ${byteLength},\n` + + ' [byteOffset]: 0,\n' + + ` [buffer]: ArrayBuffer { byteLength: ${byteLength} } ]`); + assert.strictEqual( + util.inspect(array, false), + `${constructor.name} [ 65, 97 ]` + ); +}); + +// Now check that declaring a TypedArray in a different context works the same +[ Float32Array, + Float64Array, + Int16Array, + Int32Array, + Int8Array, + Uint16Array, + Uint32Array, + Uint8Array, + Uint8ClampedArray ].forEach((constructor) => { + const length = 2; + const byteLength = length * constructor.BYTES_PER_ELEMENT; + const array = vm.runInNewContext( + 'new constructor(new ArrayBuffer(byteLength), 0, length)', + { constructor, byteLength, length } + ); + array[0] = 65; + array[1] = 97; + assert.strictEqual( + util.inspect(array, true), + `${constructor.name} [\n` + + ' 65,\n' + + ' 97,\n' + + ` [BYTES_PER_ELEMENT]: ${constructor.BYTES_PER_ELEMENT},\n` + + ` [length]: ${length},\n` + + ` [byteLength]: ${byteLength},\n` + + ' [byteOffset]: 0,\n' + + ` [buffer]: ArrayBuffer { byteLength: ${byteLength} } ]`); + assert.strictEqual( + util.inspect(array, false), + `${constructor.name} [ 65, 97 ]` + ); +}); + +assert.strictEqual( + util.inspect(Object.create({}, { + visible: { value: 1, enumerable: true }, + hidden: { value: 2 } + }), { showHidden: true }), + '{ visible: 1, [hidden]: 2 }' +); +// Objects without prototype +assert.strictEqual( + util.inspect(Object.create(null, { + name: { value: 'Tim', enumerable: true }, + hidden: { value: 'secret' } + }), { showHidden: true }), + "{ name: 'Tim', [hidden]: 'secret' }" +); + +assert.strictEqual( + util.inspect(Object.create(null, { + name: { value: 'Tim', enumerable: true }, + hidden: { value: 'secret' } + })), + '{ name: \'Tim\' }' +); + +// Dynamic properties +{ + assert.strictEqual( + util.inspect({ get readonly() {} }), + '{ readonly: [Getter] }'); + + assert.strictEqual( + util.inspect({ get readwrite() {}, set readwrite(val) {} }), + '{ readwrite: [Getter/Setter] }'); + + assert.strictEqual( + util.inspect({ set writeonly(val) {} }), + '{ writeonly: [Setter] }'); + + const value = {}; + value.a = value; + assert.strictEqual(util.inspect(value), '{ a: [Circular] }'); +} + +// Array with dynamic properties +{ + const value = [1, 2, 3]; + Object.defineProperty( + value, + 'growingLength', + { + enumerable: true, + get: function() { this.push(true); return this.length; } + } + ); + Object.defineProperty( + value, + '-1', + { + enumerable: true, + value: -1 + } + ); + assert.strictEqual(util.inspect(value), + '[ 1, 2, 3, growingLength: [Getter], \'-1\': -1 ]'); +} + +// Array with inherited number properties +{ + class CustomArray extends Array {} + CustomArray.prototype[5] = 'foo'; + const arr = new CustomArray(50); + assert.strictEqual(util.inspect(arr), 'CustomArray [ <50 empty items> ]'); +} + +// Array with extra properties +{ + const arr = [1, 2, 3, , ]; + arr.foo = 'bar'; + assert.strictEqual(util.inspect(arr), + "[ 1, 2, 3, <1 empty item>, foo: 'bar' ]"); + + const arr2 = []; + assert.strictEqual(util.inspect([], { showHidden: true }), '[ [length]: 0 ]'); + arr2['00'] = 1; + assert.strictEqual(util.inspect(arr2), "[ '00': 1 ]"); + assert.strictEqual(util.inspect(arr2, { showHidden: true }), + "[ [length]: 0, '00': 1 ]"); + arr2[1] = 0; + assert.strictEqual(util.inspect(arr2), "[ <1 empty item>, 0, '00': 1 ]"); + assert.strictEqual(util.inspect(arr2, { showHidden: true }), + "[ <1 empty item>, 0, [length]: 2, '00': 1 ]"); + delete arr2[1]; + assert.strictEqual(util.inspect(arr2), "[ <2 empty items>, '00': 1 ]"); + assert.strictEqual(util.inspect(arr2, { showHidden: true }), + "[ <2 empty items>, [length]: 2, '00': 1 ]"); + arr2['01'] = 2; + assert.strictEqual(util.inspect(arr2), + "[ <2 empty items>, '00': 1, '01': 2 ]"); + assert.strictEqual(util.inspect(arr2, { showHidden: true }), + "[ <2 empty items>, [length]: 2, '00': 1, '01': 2 ]"); + + const arr3 = []; + arr3[-1] = -1; + assert.strictEqual(util.inspect(arr3), "[ '-1': -1 ]"); +} + +// Indices out of bounds +{ + const arr = []; + arr[2 ** 32] = true; // not a valid array index + assert.strictEqual(util.inspect(arr), "[ '4294967296': true ]"); + arr[0] = true; + arr[10] = true; + assert.strictEqual(util.inspect(arr), + "[ true, <9 empty items>, true, '4294967296': true ]"); + arr[2 ** 32 - 2] = true; + arr[2 ** 32 - 1] = true; + arr[2 ** 32 + 1] = true; + delete arr[0]; + delete arr[10]; + assert.strictEqual(util.inspect(arr), + ['[ <4294967294 empty items>,', + 'true,', + "'4294967296': true,", + "'4294967295': true,", + "'4294967297': true ]" + ].join('\n ')); +} + +// Function with properties +{ + const value = () => {}; + value.aprop = 42; + assert.strictEqual(util.inspect(value), '{ [Function: value] aprop: 42 }'); +} + +// Anonymous function with properties +{ + const value = (() => function() {})(); + value.aprop = 42; + assert.strictEqual(util.inspect(value), '{ [Function] aprop: 42 }'); +} + +// Regular expressions with properties +{ + const value = /123/ig; + value.aprop = 42; + assert.strictEqual(util.inspect(value), '{ /123/gi aprop: 42 }'); +} + +// Dates with properties +{ + const value = new Date('Sun, 14 Feb 2010 11:48:40 GMT'); + value.aprop = 42; + assert.strictEqual(util.inspect(value), + '{ 2010-02-14T11:48:40.000Z aprop: 42 }'); +} // test the internal isDate implementation var Date2 = require('vm').runInNewContext('Date'); var d = new Date2(); @@ -34,12 +406,40 @@ var after = util.inspect(d); assert.equal(orig, after); // test for sparse array -var a = ['foo', 'bar', 'baz']; -assert.equal(util.inspect(a), '[ \'foo\', \'bar\', \'baz\' ]'); -delete a[1]; -assert.equal(util.inspect(a), '[ \'foo\', , \'baz\' ]'); -assert.equal(util.inspect(a, true), '[ \'foo\', , \'baz\', [length]: 3 ]'); -assert.equal(util.inspect(new Array(5)), '[ , , , , ]'); +{ + const a = ['foo', 'bar', 'baz']; + assert.strictEqual(util.inspect(a), '[ \'foo\', \'bar\', \'baz\' ]'); + delete a[1]; + assert.strictEqual(util.inspect(a), '[ \'foo\', <1 empty item>, \'baz\' ]'); + assert.strictEqual( + util.inspect(a, true), + '[ \'foo\', <1 empty item>, \'baz\', [length]: 3 ]' + ); + assert.strictEqual(util.inspect(new Array(5)), '[ <5 empty items> ]'); + a[3] = 'bar'; + a[100] = 'qux'; + assert.strictEqual( + util.inspect(a, { breakLength: Infinity }), + '[ \'foo\', <1 empty item>, \'baz\', \'bar\', <96 empty items>, \'qux\' ]' + ); + delete a[3]; + assert.strictEqual( + util.inspect(a, { maxArrayLength: 4 }), + '[ \'foo\', <1 empty item>, \'baz\', <97 empty items>, ... 1 more item ]' + ); +} + +// test for other constructors in different context +{ + let obj = vm.runInNewContext('(function(){return {}})()', {}); + assert.strictEqual(util.inspect(obj), '{}'); + obj = vm.runInNewContext('var m=new Map();m.set(1,2);m', {}); + assert.strictEqual(util.inspect(obj), 'Map { 1 => 2 }'); + obj = vm.runInNewContext('var s=new Set();s.add(1);s.add(2);s', {}); + assert.strictEqual(util.inspect(obj), 'Set { 1, 2 }'); + obj = vm.runInNewContext('fn=function(){};new Promise(fn,fn)', {}); + assert.strictEqual(util.inspect(obj), 'Promise { }'); +} // test for property descriptors var getter = Object.create(null, { @@ -63,23 +463,29 @@ assert.equal(util.inspect(setter, true), '{ [b]: [Setter] }'); assert.equal(util.inspect(getterAndSetter, true), '{ [c]: [Getter/Setter] }'); // exceptions should print the error message, not '{}' -assert.equal(util.inspect(new Error()), '[Error]'); -assert.equal(util.inspect(new Error('FAIL')), '[Error: FAIL]'); -assert.equal(util.inspect(new TypeError('FAIL')), '[TypeError: FAIL]'); -assert.equal(util.inspect(new SyntaxError('FAIL')), '[SyntaxError: FAIL]'); -try { - undef(); -} catch (e) { - assert.equal(util.inspect(e), '[ReferenceError: undef is not defined]'); -} -var ex = util.inspect(new Error('FAIL'), true); -assert.ok(ex.indexOf('[Error: FAIL]') != -1); -assert.ok(ex.indexOf('[stack]') != -1); -assert.ok(ex.indexOf('[message]') != -1); +{ + const errors = []; + errors.push(new Error()); + errors.push(new Error('FAIL')); + errors.push(new TypeError('FAIL')); + errors.push(new SyntaxError('FAIL')); + errors.forEach((err) => { + assert.strictEqual(util.inspect(err), err.stack); + }); + try { + undef(); // eslint-disable-line no-undef + } catch (e) { + assert.strictEqual(util.inspect(e), e.stack); + } + const ex = util.inspect(new Error('FAIL'), true); + assert(ex.includes('Error: FAIL')); + assert(ex.includes('[stack]')); + assert(ex.includes('[message]')); +} // GH-1941 // should not throw: -assert.equal(util.inspect(Object.create(Date.prototype)), '{}'); +assert.equal(util.inspect(Object.create(Date.prototype)), 'Date {}'); // GH-1944 assert.doesNotThrow(function() { @@ -88,6 +494,12 @@ assert.doesNotThrow(function() { util.inspect(d); }); +assert.doesNotThrow(function() { + var d = new Date(); + d.toISOString = null; + util.inspect(d); +}); + assert.doesNotThrow(function() { var r = /regexp/; r.toString = null; @@ -106,7 +518,7 @@ var x = { inspect: util.inspect }; assert.ok(util.inspect(x).indexOf('inspect') != -1); // util.inspect.styles and util.inspect.colors -function test_color_style(style, input, implicit) { +function testColorStyle(style, input, implicit) { var color_name = util.inspect.styles[style]; var color = ['', '']; if(util.inspect.colors[color_name]) @@ -119,14 +531,14 @@ function test_color_style(style, input, implicit) { assert.equal(with_color, expect, 'util.inspect color for style '+style); } -test_color_style('special', function(){}); -test_color_style('number', 123.456); -test_color_style('boolean', true); -test_color_style('undefined', undefined); -test_color_style('null', null); -test_color_style('string', 'test string'); -test_color_style('date', new Date); -test_color_style('regexp', /regexp/); +testColorStyle('special', function(){}); +testColorStyle('number', 123.456); +testColorStyle('boolean', true); +testColorStyle('undefined', undefined); +testColorStyle('null', null); +testColorStyle('string', 'test string'); +testColorStyle('date', new Date); +testColorStyle('regexp', /regexp/); // an object with "hasOwnProperty" overwritten should not throw assert.doesNotThrow(function() { diff --git a/util.js b/util.js index ca69d82..dd4237e 100644 --- a/util.js +++ b/util.js @@ -19,6 +19,94 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +var internalUtil = require('./internal/util'); +var removeColors = internalUtil.removeColors; +var getConstructorOf = internalUtil.getConstructorOf; +var isTypedArray = require('is-typed-array'); + +var propertyIsEnumerable = Object.prototype.propertyIsEnumerable; +var regExpToString = RegExp.prototype.toString; +var dateToISOString = Date.prototype.toISOString; +var errorToString = Error.prototype.toString; + +var strEscapeSequencesRegExp = /[\x00-\x1f\x27\x5c]/; +var strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g; +var keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; +var numberRegExp = /^(0|[1-9][0-9]*)$/; + +function isSet(obj) { + return objectToString(obj) === '[object Set]'; +} +function isMap(obj) { + return objectToString(obj) === '[object Map]'; +} +function isSetIterator(obj) { + return objectToString(obj) === '[object Set Iterator]'; +} +function isMapIterator(obj) { + return objectToString(obj) === '[object Map Iterator]'; +} +function isAnyArrayBuffer(obj) { + return objectToString(obj) === '[object ArrayBuffer]' || + objectToString(obj) === '[object SharedArrayBuffer]'; +} +function isDataView(obj) { + return objectToString(obj) === '[object DataView]'; +} +function isPromise(obj) { + return objectToString(obj) === '[object Promise]'; +} + +// Escaped special characters. Use empty strings to fill up unused entries. +var meta = [ + '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', + '\\u0005', '\\u0006', '\\u0007', '\\b', '\\t', + '\\n', '\\u000b', '\\f', '\\r', '\\u000e', + '\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013', + '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', + '\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d', + '\\u001e', '\\u001f', '', '', '', + '', '', '', '', "\\'", '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '\\\\' +]; + +function escapeFn (str) { + return meta[str.charCodeAt(0)]; +} + +// Escape control characters, single quotes and the backslash. +// This is similar to JSON stringify escaping. +function strEscape(str) { + // Some magic numbers that worked out fine while benchmarking with v8 6.0 + if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) + return '\'' + str + '\''; + if (str.length > 100) + return '\'' + str.replace(strEscapeSequencesReplacer, escapeFn) + '\''; + var result = ''; + var last = 0; + for (var i = 0; i < str.length; i++) { + var point = str.charCodeAt(i); + if (point === 39 || point === 92 || point < 32) { + if (last === i) { + result += meta[point]; + } else { + result += str.slice(last, i) + meta[point]; + } + last = i + 1; + } + } + if (last === 0) { + result = str; + } else if (last !== i) { + result += str.slice(last); + } + return '\'' + result + '\''; +} + var formatRegExp = /%[sdj%]/g; exports.format = function(f) { if (!isString(f)) { @@ -120,6 +208,7 @@ exports.debuglog = function(set) { return debugs[set]; }; +var customInspectSymbol = typeof Symbol !== 'undefined' ? Symbol('util.inspect.custom') : undefined; /** * Echos the value of a value. Trys to print the value out @@ -132,6 +221,8 @@ exports.debuglog = function(set) { function inspect(obj, opts) { // default options var ctx = { + maxArrayLength: 100, + breakLength: 60, seen: [], stylize: stylizeNoColor }; @@ -154,7 +245,7 @@ function inspect(obj, opts) { return formatValue(ctx, obj, ctx.depth); } exports.inspect = inspect; - +inspect.custom = customInspectSymbol; // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics inspect.colors = { @@ -214,244 +305,464 @@ function arrayToHash(array) { return hash; } +function formatValue(ctx, value, recurseTimes, ln) { + // Primitive types cannot have properties + if (typeof value !== 'object' && typeof value !== 'function') { + return formatPrimitive(ctx.stylize, value); + } + if (value === null) { + return ctx.stylize('null', 'null'); + } -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 && - isFunction(value.inspect) && - // 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 (!isString(ret)) { - ret = formatValue(ctx, ret, recurseTimes); + if (ctx.customInspect && customInspectSymbol) { + var 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)) { + var 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') { + return formatValue(ctx, ret, recurseTimes); + } + return ret; + } } - return ret; } - // Primitive types cannot have properties - var primitive = formatPrimitive(ctx, value); - if (primitive) { - return primitive; - } + var keys; + var symbols = typeof Symbol !== 'undefined' ? Object.getOwnPropertySymbols(value) : []; // Look up the keys of the object. - var keys = Object.keys(value); - var visibleKeys = arrayToHash(keys); - if (ctx.showHidden) { keys = Object.getOwnPropertyNames(value); + } else { + keys = Object.keys(value); + if (symbols.length !== 0) + symbols = symbols.filter((key) => propertyIsEnumerable.call(value, key)); } - // IE doesn't make error fields non-enumerable - // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx - if (isError(value) - && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) { - return formatError(value); - } - - // Some type of object without properties can be shortcutted. - if (keys.length === 0) { - if (isFunction(value)) { - var name = value.name ? ': ' + value.name : ''; - return ctx.stylize('[Function' + name + ']', 'special'); - } - if (isRegExp(value)) { - return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); - } - if (isDate(value)) { - return ctx.stylize(Date.prototype.toString.call(value), 'date'); + var keyLength = keys.length + symbols.length; + var constructor = getConstructorOf(value); + var ctorName = constructor && constructor.name ? + constructor.name + ' ' : ''; + + var base = ''; + var formatter = formatObject; + var braces; + var noIterator = true; + var raw; + + // Iterators and the rest are split to reduce checks + if (value[Symbol.iterator]) { + noIterator = false; + if (Array.isArray(value)) { + // Only set the constructor for non ordinary ("Array [...]") arrays. + braces = [(ctorName === 'Array ' ? '' : ctorName) + '[', ']']; + if (value.length === 0 && keyLength === 0) + return braces[0] + ']'; + formatter = formatArray; + } else if (isSet(value)) { + if (value.size === 0 && keyLength === 0) + return ctorName + '{}'; + braces = [ctorName + '{', '}']; + formatter = formatSet; + } else if (isMap(value)) { + if (value.size === 0 && keyLength === 0) + return ctorName + '{}'; + braces = [ctorName + '{', '}']; + formatter = formatMap; + } else if (isTypedArray(value)) { + braces = [ctorName + '[', ']']; + formatter = formatTypedArray; + } else if (isMapIterator(value)) { + braces = ['MapIterator {', '}']; + formatter = formatCollectionIterator; + } else if (isSetIterator(value)) { + braces = ['SetIterator {', '}']; + formatter = formatCollectionIterator; + } else { + // Check for boxed strings with valueOf() + // The .valueOf() call can fail for a multitude of reasons + try { + raw = value.valueOf(); + } catch (e) { /* ignore */ } + + if (typeof raw === 'string') { + var formatted = formatPrimitive(stylizeNoColor, raw); + if (keyLength === raw.length) + return ctx.stylize('[String: ' + formatted + ']', 'string'); + base = ' [String: ' + formatted + ']'; + // For boxed Strings, we have to remove the 0-n indexed entries, + // since they just noisy up the output and are redundant + // Make boxed primitive Strings look like such + keys = keys.slice(value.length); + braces = ['{', '}']; + } else { + noIterator = true; + } } - if (isError(value)) { - return formatError(value); + } + if (noIterator) { + braces = ['{', '}']; + if (ctorName === 'Object ') { + // Object fast path + if (keyLength === 0) + return '{}'; + } else if (typeof value === 'function') { + var name = constructor.name + (value.name ? ': ' + value.name : ''); + if (keyLength === 0) + return ctx.stylize('[' + name + ']', 'special'); + base = ' [' + name + ']'; + } else if (isRegExp(value)) { + // Make RegExps say that they are RegExps + if (keyLength === 0 || recurseTimes < 0) + return ctx.stylize(regExpToString.call(value), 'regexp'); + base = ' ' + regExpToString.call(value); + } else if (isDate(value)) { + if (keyLength === 0) { + if (Number.isNaN(value.getTime())) + return ctx.stylize(value.toString(), 'date'); + return ctx.stylize(dateToISOString.call(value), 'date'); + } + // Make dates with properties first say the date + base = ' ' + dateToISOString.call(value); + } else if (isError(value)) { + // Make error with message first say the error + if (keyLength === 0) + return formatError(value); + base = ' ' + formatError(value); + } else if (isAnyArrayBuffer(value)) { + // Fast path for ArrayBuffer and SharedArrayBuffer. + // Can't do the same for DataView because it has a non-primitive + // .buffer property that we need to recurse for. + if (keyLength === 0) + return ctorName + + '{ byteLength: ' + formatNumber(ctx.stylize, value.byteLength) + ' }'; + braces[0] = ctorName + '{'; + keys.unshift('byteLength'); + } else if (isDataView(value)) { + braces[0] = ctorName + '{'; + // .buffer goes last, it's not a primitive like the others. + keys.unshift('byteLength', 'byteOffset', 'buffer'); + } else if (isPromise(value)) { + braces[0] = ctorName + '{'; + formatter = formatPromise; + } else { + // Check boxed primitives other than string with valueOf() + // NOTE: `Date` has to be checked first! + // The .valueOf() call can fail for a multitude of reasons + try { + raw = value.valueOf(); + } catch (e) { /* ignore */ } + + if (typeof raw === 'number') { + // Make boxed primitive Numbers look like such + var formatted = formatPrimitive(stylizeNoColor, raw); + if (keyLength === 0) + return ctx.stylize(`[Number: ${formatted}]`, 'number'); + base = ` [Number: ${formatted}]`; + } else if (typeof raw === 'boolean') { + // Make boxed primitive Booleans look like such + var formatted = formatPrimitive(stylizeNoColor, raw); + if (keyLength === 0) + return ctx.stylize('[Boolean: ' + formatted + ']', 'boolean'); + base = ' [Boolean: ' + formatted + ']'; + } else if (typeof raw === 'symbol') { + var formatted = formatPrimitive(stylizeNoColor, raw); + return ctx.stylize('[Symbol: ' + formatted + ']', 'symbol'); + } else if (keyLength === 0) { + return ctorName + '{}'; + } else { + braces[0] = ctorName + '{'; + } } } - var base = '', array = false, braces = ['{', '}']; - - // Make Array say that they are Array - if (isArray(value)) { - array = true; - braces = ['[', ']']; - } + // Using an array here is actually better for the average case than using + // a Set. `seen` will only check for the depth and will never grow to large. + if (ctx.seen.indexOf(value) !== -1) + return ctx.stylize('[Circular]', 'special'); - // Make functions say that they are functions - if (isFunction(value)) { - var n = value.name ? ': ' + value.name : ''; - base = ' [Function' + n + ']'; + if (recurseTimes != null) { + if (recurseTimes < 0) + return ctx.stylize('[' + (constructor ? constructor.name : 'Object') + ']', + 'special'); + recurseTimes -= 1; } - // Make RegExps say that they are RegExps - if (isRegExp(value)) { - base = ' ' + RegExp.prototype.toString.call(value); - } + ctx.seen.push(value); + var output = formatter(ctx, value, recurseTimes, keys); - // Make dates with properties first say the date - if (isDate(value)) { - base = ' ' + Date.prototype.toUTCString.call(value); + for (var i = 0; i < symbols.length; i++) { + output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0)); } + ctx.seen.pop(); - // Make error with message first say the error - if (isError(value)) { - base = ' ' + formatError(value); - } + return reduceToSingleString(ctx, output, base, braces, ln); +} - if (keys.length === 0 && (!array || value.length == 0)) { - return braces[0] + base + braces[1]; - } - if (recurseTimes < 0) { - if (isRegExp(value)) { - return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); - } else { - return ctx.stylize('[Object]', 'special'); - } - } - ctx.seen.push(value); +function formatNumber(fn, value) { + // Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0. + if (value === 0 && (1 / value) === -Infinity) + return fn('-0', 'number'); + return fn(String(value), 'number'); +} - var output; - if (array) { - output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); - } else { - output = keys.map(function(key) { - return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); - }); - } +function formatPrimitive(fn, value) { + if (typeof value === 'string') + return fn(strEscape(value), 'string'); + if (typeof value === 'number') + return formatNumber(fn, value); + if (typeof value === 'boolean') + return fn(String(value), 'boolean'); + if (typeof value === 'undefined') + return fn('undefined', 'undefined'); + // es6 symbol primitive + return fn(value.toString(), 'symbol'); +} - ctx.seen.pop(); - return reduceToSingleString(output, base, braces); +function formatError(value) { + return value.stack || '[' + errorToString.call(value) + ']'; } +function formatObject(ctx, value, recurseTimes, keys) { + var len = keys.length; + var output = new Array(len); + for (var i = 0; i < len; i++) + output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 0); + return output; +} -function formatPrimitive(ctx, value) { - if (isUndefined(value)) - return ctx.stylize('undefined', 'undefined'); - if (isString(value)) { - var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') - .replace(/'/g, "\\'") - .replace(/\\"/g, '"') + '\''; - return ctx.stylize(simple, 'string'); +// The array is sparse and/or has extra keys +function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) { + var output = []; + var keyLen = keys.length; + var visibleLength = 0; + var i = 0; + if (keyLen !== 0 && numberRegExp.test(keys[0])) { + for (var key of keys) { + if (visibleLength === maxLength) + break; + var index = +key; + // Arrays can only have up to 2^32 - 1 entries + if (index > 2 ** 32 - 2) + break; + if (i !== index) { + if (!numberRegExp.test(key)) + break; + var emptyItems = index - i; + var ending = emptyItems > 1 ? 's' : ''; + var message = '<' + emptyItems + ' empty item' + ending + '>'; + output.push(ctx.stylize(message, 'undefined')); + i = index; + if (++visibleLength === maxLength) + break; + } + output.push(formatProperty(ctx, value, recurseTimes, key, 1)); + visibleLength++; + i++; + } } - if (isNumber(value)) - return ctx.stylize('' + value, 'number'); - if (isBoolean(value)) - return ctx.stylize('' + value, 'boolean'); - // For some reason typeof null is "object", so special case here. - if (isNull(value)) - return ctx.stylize('null', 'null'); + if (i < valLen && visibleLength !== maxLength) { + var len = valLen - i; + var ending = len > 1 ? 's' : ''; + var message = '<' + len + ' empty item' + ending + '>'; + output.push(ctx.stylize(message, 'undefined')); + i = valLen; + if (keyLen === 0) + return output; + } + var remaining = valLen - i; + if (remaining > 0) { + output.push('... ' + remaining + ' more item' + (remaining > 1 ? 's' : '')); + } + if (ctx.showHidden && keys[keyLen - 1] === 'length') { + // No extra keys + output.push(formatProperty(ctx, value, recurseTimes, 'length', 2)); + } else if (valLen === 0 || + keyLen > valLen && keys[valLen - 1] === String(valLen - 1)) { + // The array is not sparse + for (i = valLen; i < keyLen; i++) + output.push(formatProperty(ctx, value, recurseTimes, keys[i], 2)); + } else if (keys[keyLen - 1] !== String(valLen - 1)) { + var extra = []; + // Only handle special keys + var key; + for (i = keys.length - 1; i >= 0; i--) { + key = keys[i]; + if (numberRegExp.test(key) && +key < Math.pow(2, 32) - 1) + break; + extra.push(formatProperty(ctx, value, recurseTimes, key, 2)); + } + for (i = extra.length - 1; i >= 0; i--) + output.push(extra[i]); + } + return output; } +function formatArray(ctx, value, recurseTimes, keys) { + var len = Math.min(Math.max(0, ctx.maxArrayLength), value.length); + var hidden = ctx.showHidden ? 1 : 0; + var valLen = value.length; + var keyLen = keys.length - hidden; + if (keyLen !== valLen || keys[keyLen - 1] !== String(valLen - 1)) + return formatSpecialArray(ctx, value, recurseTimes, keys, len, valLen); + + var remaining = valLen - len; + var output = new Array(len + (remaining > 0 ? 1 : 0) + hidden); + for (var i = 0; i < len; i++) + output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 1); + if (remaining > 0) + output[i++] = '... ' + remaining + ' more item' + (remaining > 1 ? 's' : ''); + if (ctx.showHidden === true) + output[i] = formatProperty(ctx, value, recurseTimes, 'length', 2); + return output; +} -function formatError(value) { - return '[' + Error.prototype.toString.call(value) + ']'; +function formatTypedArray(ctx, value, recurseTimes, keys) { + var maxLength = Math.min(Math.max(0, ctx.maxArrayLength), value.length); + var remaining = value.length - maxLength; + var output = new Array(maxLength + (remaining > 0 ? 1 : 0)); + for (var i = 0; i < maxLength; ++i) + output[i] = formatNumber(ctx.stylize, value[i]); + if (remaining > 0) + output[i] = '... ' + remaining + ' more item' + (remaining > 1 ? 's' : ''); + if (ctx.showHidden) { + // .buffer goes last, it's not a primitive like the others. + var extraKeys = [ + 'BYTES_PER_ELEMENT', + 'length', + 'byteLength', + 'byteOffset', + 'buffer' + ]; + for (i = 0; i < extraKeys.length; i++) { + var str = formatValue(ctx, value[extraKeys[i]], recurseTimes); + output.push('[' + extraKeys[i] + ']: ' + str); + } + } + // TypedArrays cannot have holes. Therefore it is safe to assume that all + // extra keys are indexed after value.length. + for (i = value.length; i < keys.length; i++) { + output.push(formatProperty(ctx, value, recurseTimes, keys[i], 2)); + } + return output; } +function formatSet(ctx, value, recurseTimes, keys) { + var output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); + var i = 0; + for (var v of value) + output[i++] = formatValue(ctx, v, recurseTimes); + // With `showHidden`, `length` will display as a hidden property for + // arrays. For consistency's sake, do the same for `size`, even though this + // property isn't selected by Object.getOwnPropertyNames(). + if (ctx.showHidden) + output[i++] = '[size]: ' + ctx.stylize(String(value.size), 'number'); + for (var n = 0; n < keys.length; n++) { + output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); + } + return output; +} -function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { - var output = []; - for (var i = 0, l = value.length; i < l; ++i) { - if (hasOwnProperty(value, String(i))) { - output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, - String(i), true)); - } else { - output.push(''); - } +function formatMap(ctx, value, recurseTimes, keys) { + var output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); + var i = 0; + for (var [k, v] of value) + output[i++] = formatValue(ctx, k, recurseTimes) + ' => ' + + formatValue(ctx, v, recurseTimes); + // See comment in formatSet + if (ctx.showHidden) + output[i++] = '[size]: ' + ctx.stylize(String(value.size), 'number'); + for (var n = 0; n < keys.length; n++) { + output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); } - keys.forEach(function(key) { - if (!key.match(/^\d+$/)) { - output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, - key, true)); - } - }); return output; } +function formatPromise(ctx, value, recurseTimes, keys) { + var output = []; + for (var n = 0; n < keys.length; n++) { + output.push(formatProperty(ctx, value, recurseTimes, keys[n], 0)); + } + return output; +} -function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { - var name, str, desc; - desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; - if (desc.get) { - if (desc.set) { +function formatProperty(ctx, value, recurseTimes, key, array) { + var name, str; + var desc = Object.getOwnPropertyDescriptor(value, key) || + { value: value[key], enumerable: true }; + if (desc.value !== undefined) { + var diff = array === 0 ? 3 : 2; + ctx.indentationLvl += diff; + str = formatValue(ctx, desc.value, recurseTimes, array === 0); + ctx.indentationLvl -= diff; + } else if (desc.get !== undefined) { + if (desc.set !== undefined) { str = ctx.stylize('[Getter/Setter]', 'special'); } else { str = ctx.stylize('[Getter]', 'special'); } + } else if (desc.set !== undefined) { + str = ctx.stylize('[Setter]', 'special'); } else { - if (desc.set) { - str = ctx.stylize('[Setter]', 'special'); - } + str = ctx.stylize('undefined', 'undefined'); } - if (!hasOwnProperty(visibleKeys, key)) { - name = '[' + key + ']'; - } - if (!str) { - if (ctx.seen.indexOf(desc.value) < 0) { - if (isNull(recurseTimes)) { - str = formatValue(ctx, desc.value, null); - } else { - str = formatValue(ctx, desc.value, recurseTimes - 1); - } - if (str.indexOf('\n') > -1) { - if (array) { - str = str.split('\n').map(function(line) { - return ' ' + line; - }).join('\n').substr(2); - } else { - str = '\n' + str.split('\n').map(function(line) { - return ' ' + line; - }).join('\n'); - } - } - } else { - str = ctx.stylize('[Circular]', 'special'); - } + if (array === 1) { + return str; } - if (isUndefined(name)) { - if (array && key.match(/^\d+$/)) { - return str; - } - name = JSON.stringify('' + key); - if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { - name = name.substr(1, name.length - 2); - name = ctx.stylize(name, 'name'); - } else { - name = name.replace(/'/g, "\\'") - .replace(/\\"/g, '"') - .replace(/(^"|"$)/g, "'"); - name = ctx.stylize(name, 'string'); - } + if (typeof key === 'symbol') { + name = '[' + ctx.stylize(key.toString(), 'symbol') + ']'; + } else if (desc.enumerable === false) { + name = '[' + key + ']'; + } else if (keyStrRegExp.test(key)) { + name = ctx.stylize(key, 'name'); + } else { + name = ctx.stylize(strEscape(key), 'string'); } return name + ': ' + str; } -function reduceToSingleString(output, base, braces) { - var numLinesEst = 0; - var length = output.reduce(function(prev, cur) { - numLinesEst++; - if (cur.indexOf('\n') >= 0) numLinesEst++; - return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; - }, 0); - - if (length > 60) { - return braces[0] + - (base === '' ? '' : base + '\n ') + - ' ' + - output.join(',\n ') + - ' ' + - braces[1]; +function reduceToSingleString(ctx, output, base, braces, addLn) { + var breakLength = ctx.breakLength; + if (output.length * 2 <= breakLength) { + var length = 0; + for (var i = 0; i < output.length && length <= breakLength; i++) { + if (ctx.colors) { + length += removeColors(output[i]).length + 1; + } else { + length += output[i].length + 1; + } + } + if (length <= breakLength) + return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; } - - return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; + // If the opening "brace" is too large, like in the case of "Set {", + // we need to force the first item to be on the next line or the + // items will not line up correctly. + var indentation = ' '.repeat(ctx.indentationLvl); + var extraLn = addLn === true ? '\n' + indentation : ''; + var ln = base === '' && braces[0].length === 1 ? + ' ' : base + '\n' + indentation + ' '; + var str = output.join(',\n' + indentation + ' '); + return extraLn + braces[0] + ln + str + ' ' + braces[1]; } + // NOTE: These type checking functions intentionally don't use `instanceof` // because it is fragile and can be easily faked with `Object.create()`. function isArray(ar) {