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 89053c7..856410c 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "test:browsers": "airtap test/browser/index.js" }, "dependencies": { - "inherits": "2.0.3" + "inherits": "2.0.3", + "is-typed-array": "1.0.4" }, "license": "MIT", "devDependencies": { diff --git a/test/node/format.js b/test/node/format.js index f2d1862..4672604 100644 --- a/test/node/format.js +++ b/test/node/format.js @@ -67,11 +67,13 @@ assert.equal(util.format('%%%s%%%%', 'hi'), '%hi%%'); })(); // Errors -assert.equal(util.format(new Error('foo')), '[Error: foo]'); +var foo = new Error('foo'); +assert.equal(util.format(foo), foo.stack); function CustomError(msg) { Error.call(this); Object.defineProperty(this, 'message', { value: msg, enumerable: false }); Object.defineProperty(this, 'name', { value: 'CustomError', enumerable: false }); } util.inherits(CustomError, Error); -assert.equal(util.format(new CustomError('bar')), '[CustomError: bar]'); +var custom = new CustomError('bar'); +assert.equal(util.format(custom), '[CustomError: bar]'); diff --git a/test/node/inspect.js b/test/node/inspect.js index 8f4663b..544d83f 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,25 +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); -/* Unsure why these don't work -assert.ok(ex.indexOf('[stack]') != -1); -assert.ok(ex.indexOf('[message]') != -1); -*/ +{ + var 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); + } + var ex = util.inspect(new Error('FAIL'), true); + assert(ex.indexOf('Error: FAIL') !== -1); + assert(ex.indexOf('[stack]') !== -1); + assert(ex.indexOf('[message]') !== -1); +} // 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() { @@ -90,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; @@ -108,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]) @@ -121,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 cbc2498..d33c15d 100644 --- a/util.js +++ b/util.js @@ -29,6 +29,94 @@ var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || return descriptors; }; +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)) { @@ -123,6 +211,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 @@ -135,6 +224,8 @@ exports.debuglog = function(set) { function inspect(obj, opts) { // default options var ctx = { + maxArrayLength: 100, + breakLength: 60, seen: [], stylize: stylizeNoColor }; @@ -157,7 +248,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 = { @@ -217,244 +308,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); - } - - // 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'); + } else { + keys = Object.keys(value); + if (symbols.length !== 0) + symbols = symbols.filter((key) => propertyIsEnumerable.call(value, key)); + } + + 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 = ['{', '}']; + // 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 Array say that they are Array - if (isArray(value)) { - array = true; - braces = ['[', ']']; + if (recurseTimes != null) { + if (recurseTimes < 0) + return ctx.stylize('[' + (constructor ? constructor.name : 'Object') + ']', + 'special'); + recurseTimes -= 1; } - // Make functions say that they are functions - if (isFunction(value)) { - var n = value.name ? ': ' + value.name : ''; - base = ' [Function' + n + ']'; - } - - // 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]; - } - - return braces[0] + base + ' ' + output.join(', ') + ' ' + 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]; + } + // 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) {