diff --git a/index.js b/index.js index a4b2d4c..46ea476 100644 --- a/index.js +++ b/index.js @@ -2,10 +2,12 @@ var hasMap = typeof Map === 'function' && Map.prototype; var mapSizeDescriptor = Object.getOwnPropertyDescriptor && hasMap ? Object.getOwnPropertyDescriptor(Map.prototype, 'size') : null; var mapSize = hasMap && mapSizeDescriptor && typeof mapSizeDescriptor.get === 'function' ? mapSizeDescriptor.get : null; var mapForEach = hasMap && Map.prototype.forEach; +var mapEntries = hasMap && Map.prototype.entries; var hasSet = typeof Set === 'function' && Set.prototype; var setSizeDescriptor = Object.getOwnPropertyDescriptor && hasSet ? Object.getOwnPropertyDescriptor(Set.prototype, 'size') : null; var setSize = hasSet && setSizeDescriptor && typeof setSizeDescriptor.get === 'function' ? setSizeDescriptor.get : null; var setForEach = hasSet && Set.prototype.forEach; +var setValues = hasSet && Set.prototype.values; var hasWeakMap = typeof WeakMap === 'function' && WeakMap.prototype; var weakMapHas = hasWeakMap ? WeakMap.prototype.has : null; var hasWeakSet = typeof WeakSet === 'function' && WeakSet.prototype; @@ -112,6 +114,21 @@ module.exports = function inspect_(obj, options, depth, seen) { } var numericSeparator = opts.numericSeparator; + if ( + has(opts, 'maxArrayLength') + && opts.maxArrayLength !== null + && opts.maxArrayLength !== Infinity + && ( + typeof opts.maxArrayLength !== 'number' + || opts.maxArrayLength < 0 + || opts.maxArrayLength !== opts.maxArrayLength // NaN + || parseInt(opts.maxArrayLength, 10) !== opts.maxArrayLength // non-integer + ) + ) { + throw new TypeError('option "maxArrayLength", if provided, must be a non-negative integer, Infinity, or `null`'); + } + var maxArrayLength = typeof opts.maxArrayLength === 'number' ? opts.maxArrayLength : Infinity; + if (typeof obj === 'undefined') { return 'undefined'; } @@ -190,12 +207,45 @@ module.exports = function inspect_(obj, options, depth, seen) { } if (isArray(obj)) { if (obj.length === 0) { return '[]'; } - var xs = arrObjKeys(obj, inspect); + var xs = arrObjKeys(obj, inspect, maxArrayLength); if (indent && !singleLineValues(xs)) { return '[' + indentedJoin(xs, indent) + ']'; } return '[ ' + $join.call(xs, ', ') + ' ]'; } + if (isTypedArray(obj)) { + var typedTag = $slice.call(toStr(obj), 8, -1); // e.g., 'Uint8Array' + if (obj.length === 0) { return typedTag + ' []'; } + var typedXs = []; + var typedLimit = maxArrayLength < obj.length ? maxArrayLength : obj.length; + for (var ti = 0; ti < typedLimit; ti++) { + typedXs.push(String(obj[ti])); + } + if (obj.length > maxArrayLength) { + var typedRemaining = obj.length - maxArrayLength; + typedXs.push('... ' + typedRemaining + ' more item' + (typedRemaining > 1 ? 's' : '')); + } + if (indent && !singleLineValues(typedXs)) { + return typedTag + ' [' + indentedJoin(typedXs, indent) + ']'; + } + return typedTag + ' [ ' + $join.call(typedXs, ', ') + ' ]'; + } + if (isArguments(obj)) { + if (obj.length === 0) { return 'Arguments []'; } + var argXs = []; + var argLimit = maxArrayLength < obj.length ? maxArrayLength : obj.length; + for (var ai = 0; ai < argLimit; ai++) { + argXs.push(has(obj, ai) ? inspect(obj[ai], obj) : ''); + } + if (obj.length > maxArrayLength) { + var argRemaining = obj.length - maxArrayLength; + argXs.push('... ' + argRemaining + ' more item' + (argRemaining > 1 ? 's' : '')); + } + if (indent && !singleLineValues(argXs)) { + return 'Arguments [' + indentedJoin(argXs, indent) + ']'; + } + return 'Arguments [ ' + $join.call(argXs, ', ') + ' ]'; + } if (isError(obj)) { var parts = arrObjKeys(obj, inspect); if (!('cause' in Error.prototype) && 'cause' in obj && !isEnumerable.call(obj, 'cause')) { @@ -213,21 +263,53 @@ module.exports = function inspect_(obj, options, depth, seen) { } if (isMap(obj)) { var mapParts = []; - if (mapForEach) { + var mapLen = mapSize.call(obj); + var mapCount = 0; + if (mapEntries) { + var mapIter = mapEntries.call(obj); + var mapEntry; + while (mapCount < maxArrayLength && !(mapEntry = mapIter.next()).done) { + mapParts.push(inspect(mapEntry.value[0], obj, true) + ' => ' + inspect(mapEntry.value[1], obj)); + mapCount += 1; + } + } else if (mapForEach) { mapForEach.call(obj, function (value, key) { - mapParts.push(inspect(key, obj, true) + ' => ' + inspect(value, obj)); + if (mapCount < maxArrayLength) { + mapParts.push(inspect(key, obj, true) + ' => ' + inspect(value, obj)); + } + mapCount += 1; }); } - return collectionOf('Map', mapSize.call(obj), mapParts, indent); + if (mapLen > maxArrayLength) { + var mapRemaining = mapLen - maxArrayLength; + mapParts.push('... ' + mapRemaining + ' more item' + (mapRemaining > 1 ? 's' : '')); + } + return collectionOf('Map', mapLen, mapParts, indent); } if (isSet(obj)) { var setParts = []; - if (setForEach) { + var setLen = setSize.call(obj); + var setCount = 0; + if (setValues) { + var setIter = setValues.call(obj); + var setEntry; + while (setCount < maxArrayLength && !(setEntry = setIter.next()).done) { + setParts.push(inspect(setEntry.value, obj)); + setCount += 1; + } + } else if (setForEach) { setForEach.call(obj, function (value) { - setParts.push(inspect(value, obj)); + if (setCount < maxArrayLength) { + setParts.push(inspect(value, obj)); + } + setCount += 1; }); } - return collectionOf('Set', setSize.call(obj), setParts, indent); + if (setLen > maxArrayLength) { + var setRemaining = setLen - maxArrayLength; + setParts.push('... ' + setRemaining + ' more item' + (setRemaining > 1 ? 's' : '')); + } + return collectionOf('Set', setLen, setParts, indent); } if (isWeakMap(obj)) { return weakCollectionOf('WeakMap'); @@ -297,6 +379,21 @@ function isError(obj) { return toStr(obj) === '[object Error]' && canTrustToStri function isString(obj) { return toStr(obj) === '[object String]' && canTrustToString(obj); } function isNumber(obj) { return toStr(obj) === '[object Number]' && canTrustToString(obj); } function isBoolean(obj) { return toStr(obj) === '[object Boolean]' && canTrustToString(obj); } +function isArguments(obj) { return toStr(obj) === '[object Arguments]'; } +function isTypedArray(obj) { + var tag = toStr(obj); + return tag === '[object Int8Array]' + || tag === '[object Uint8Array]' + || tag === '[object Uint8ClampedArray]' + || tag === '[object Int16Array]' + || tag === '[object Uint16Array]' + || tag === '[object Int32Array]' + || tag === '[object Uint32Array]' + || tag === '[object Float32Array]' + || tag === '[object Float64Array]' + || tag === '[object BigInt64Array]' + || tag === '[object BigUint64Array]'; +} // Symbol and BigInt do have Symbol.toStringTag by spec, so that can't be used to eliminate false positives function isSymbol(obj) { @@ -503,14 +600,19 @@ function indentedJoin(xs, indent) { return lineJoiner + $join.call(xs, ',' + lineJoiner) + '\n' + indent.prev; } -function arrObjKeys(obj, inspect) { +function arrObjKeys(obj, inspect, maxLength) { var isArr = isArray(obj); var xs = []; if (isArr) { - xs.length = obj.length; - for (var i = 0; i < obj.length; i++) { + var limit = typeof maxLength === 'number' && maxLength < obj.length ? maxLength : obj.length; + xs.length = limit; + for (var i = 0; i < limit; i++) { xs[i] = has(obj, i) ? inspect(obj[i], obj) : ''; } + if (typeof maxLength === 'number' && obj.length > maxLength) { + var remaining = obj.length - maxLength; + xs.push('... ' + remaining + ' more item' + (remaining > 1 ? 's' : '')); + } } var syms = typeof gOPS === 'function' ? gOPS(obj) : []; var symMap; diff --git a/readme.markdown b/readme.markdown index ad35705..10cf7bd 100644 --- a/readme.markdown +++ b/readme.markdown @@ -51,6 +51,7 @@ Return a string `s` with the string representation of `obj` up to a depth of `op Additional options: - `quoteStyle`: must be "single" or "double", if present. Default `'single'` for strings, `'double'` for HTML elements. - `maxStringLength`: must be `0`, a positive integer, `Infinity`, or `null`, if present. Default `Infinity`. + - `maxArrayLength`: must be a non-negative integer, `Infinity`, or `null`, if present. Default `Infinity`. Specifies the maximum number of elements to include when formatting Arrays, TypedArrays, arguments objects, Sets, and Maps. Elements beyond this limit will be replaced with `... N more items`. - `customInspect`: When `true`, a custom inspect method function will be invoked (either undere the `util.inspect.custom` symbol, or the `inspect` property). When the string `'symbol'`, only the symbol method will be invoked. Default `true`. - `indent`: must be "\t", `null`, or a positive integer. Default `null`. - `numericSeparator`: must be a boolean, if present. Default `false`. If `true`, all numbers will be printed with numeric separators (eg, `1234.5678` will be printed as `'1_234.567_8'`) diff --git a/test/maxArrayLength.js b/test/maxArrayLength.js new file mode 100644 index 0000000..a1b8cf6 --- /dev/null +++ b/test/maxArrayLength.js @@ -0,0 +1,466 @@ +var test = require('tape'); +var forEach = require('for-each'); + +var inspect = require('../'); + +test('bad maxArrayLength options', function (t) { + forEach([ + -1, + -Infinity, + NaN, + 1.5, + 'string', + true, + false, + {}, + [] + ], function (maxArrayLength) { + t['throws']( + function () { inspect('', { maxArrayLength: maxArrayLength }); }, + TypeError, + inspect(maxArrayLength) + ' is invalid' + ); + }); + + t.end(); +}); + +test('valid maxArrayLength options', function (t) { + forEach([ + 0, + 1, + 10, + Infinity, + null + ], function (maxArrayLength) { + t.doesNotThrow( + function () { inspect([], { maxArrayLength: maxArrayLength }); }, + inspect(maxArrayLength) + ' is valid' + ); + }); + + t.end(); +}); + +test('maxArrayLength with arrays', function (t) { + var arr = [1, 2, 3, 4, 5]; + + t.equal( + inspect(arr, { maxArrayLength: 3 }), + '[ 1, 2, 3, ... 2 more items ]', + 'array truncated at maxArrayLength' + ); + + t.equal( + inspect(arr, { maxArrayLength: 5 }), + '[ 1, 2, 3, 4, 5 ]', + 'array not truncated when length equals maxArrayLength' + ); + + t.equal( + inspect(arr, { maxArrayLength: 10 }), + '[ 1, 2, 3, 4, 5 ]', + 'array not truncated when maxArrayLength exceeds length' + ); + + t.equal( + inspect(arr, { maxArrayLength: 0 }), + '[ ... 5 more items ]', + 'maxArrayLength of 0 shows all as truncated' + ); + + t.equal( + inspect([1], { maxArrayLength: 0 }), + '[ ... 1 more item ]', + 'singular item message when one item truncated' + ); + + t.equal( + inspect(arr, { maxArrayLength: Infinity }), + '[ 1, 2, 3, 4, 5 ]', + 'Infinity shows all elements' + ); + + t.equal( + inspect(arr, { maxArrayLength: null }), + '[ 1, 2, 3, 4, 5 ]', + 'null shows all elements (default behavior)' + ); + + t.end(); +}); + +test('maxArrayLength with Map', { skip: typeof Map !== 'function' }, function (t) { + var map = new Map(); + map.set('a', 1); + map.set('b', 2); + map.set('c', 3); + map.set('d', 4); + + t.equal( + inspect(map, { maxArrayLength: 2 }), + "Map (4) {'a' => 1, 'b' => 2, ... 2 more items}", + 'Map truncated at maxArrayLength' + ); + + t.equal( + inspect(map, { maxArrayLength: 4 }), + "Map (4) {'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4}", + 'Map not truncated when length equals maxArrayLength' + ); + + t.end(); +}); + +test('maxArrayLength with Set', { skip: typeof Set !== 'function' }, function (t) { + var set = new Set(); + set.add(1); + set.add(2); + set.add(3); + set.add(4); + + t.equal( + inspect(set, { maxArrayLength: 2 }), + 'Set (4) {1, 2, ... 2 more items}', + 'Set truncated at maxArrayLength' + ); + + t.equal( + inspect(set, { maxArrayLength: 4 }), + 'Set (4) {1, 2, 3, 4}', + 'Set not truncated when length equals maxArrayLength' + ); + + t.end(); +}); + +test('maxArrayLength with nested arrays', function (t) { + var arr = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; + + // maxArrayLength applies to all arrays at all depths (like util.inspect) + t.equal( + inspect(arr, { maxArrayLength: 2 }), + '[ [ 1, 2, ... 1 more item ], [ 4, 5, ... 1 more item ], ... 1 more item ]', + 'maxArrayLength applied recursively to nested arrays' + ); + + t.end(); +}); + +test('maxArrayLength with empty collections', function (t) { + t.equal( + inspect([], { maxArrayLength: 0 }), + '[]', + 'empty array with maxArrayLength 0' + ); + + t.equal( + inspect([], { maxArrayLength: 5 }), + '[]', + 'empty array with maxArrayLength 5' + ); + + t.end(); +}); + +test('maxArrayLength with empty Map', { skip: typeof Map !== 'function' }, function (t) { + var emptyMap = new Map(); + + t.equal( + inspect(emptyMap, { maxArrayLength: 0 }), + 'Map (0) {}', + 'empty Map with maxArrayLength 0' + ); + + t.end(); +}); + +test('maxArrayLength with empty Set', { skip: typeof Set !== 'function' }, function (t) { + var emptySet = new Set(); + + t.equal( + inspect(emptySet, { maxArrayLength: 0 }), + 'Set (0) {}', + 'empty Set with maxArrayLength 0' + ); + + t.end(); +}); + +test('maxArrayLength of 1', function (t) { + t.equal( + inspect([1, 2, 3], { maxArrayLength: 1 }), + '[ 1, ... 2 more items ]', + 'array with maxArrayLength 1' + ); + + t.equal( + inspect([1], { maxArrayLength: 1 }), + '[ 1 ]', + 'single element array with maxArrayLength 1' + ); + + t.end(); +}); + +test('maxArrayLength of 1 with Map', { skip: typeof Map !== 'function' }, function (t) { + var map = new Map(); + map.set('a', 1); + map.set('b', 2); + + t.equal( + inspect(map, { maxArrayLength: 1 }), + "Map (2) {'a' => 1, ... 1 more item}", + 'Map with maxArrayLength 1' + ); + + t.end(); +}); + +test('maxArrayLength of 1 with Set', { skip: typeof Set !== 'function' }, function (t) { + var set = new Set([1, 2]); + + t.equal( + inspect(set, { maxArrayLength: 1 }), + 'Set (2) {1, ... 1 more item}', + 'Set with maxArrayLength 1' + ); + + t.end(); +}); + +test('maxArrayLength with sparse arrays', function (t) { + var sparse = [1, , , 4, 5]; // eslint-disable-line no-sparse-arrays + + t.equal( + inspect(sparse, { maxArrayLength: 3 }), + '[ 1, , , ... 2 more items ]', + 'sparse array truncated at maxArrayLength' + ); + + t.equal( + inspect(sparse, { maxArrayLength: 2 }), + '[ 1, , ... 3 more items ]', + 'sparse array with maxArrayLength 2' + ); + + t.end(); +}); + +test('maxArrayLength with arrays containing undefined and null', function (t) { + var arr = [undefined, null, 1, undefined, null]; + + t.equal( + inspect(arr, { maxArrayLength: 3 }), + '[ undefined, null, 1, ... 2 more items ]', + 'array with undefined/null truncated at maxArrayLength' + ); + + t.equal( + inspect(arr, { maxArrayLength: 2 }), + '[ undefined, null, ... 3 more items ]', + 'array with undefined/null at maxArrayLength 2' + ); + + t.end(); +}); + +test('maxArrayLength with indent option', function (t) { + var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + var result = inspect(arr, { maxArrayLength: 5, indent: 2 }); + t.ok( + result.indexOf('... 5 more items') > -1, + 'truncation message present with indent' + ); + + // With enough items shown, indent should produce newlines + var longArr = []; + for (var i = 0; i < 20; i++) { + longArr.push({ key: 'value' + i }); + } + var longResult = inspect(longArr, { maxArrayLength: 10, indent: 2 }); + t.ok( + longResult.indexOf('\n') > -1, + 'newlines present with indent on complex array' + ); + + t.end(); +}); + +test('maxArrayLength with depth option', function (t) { + var nested = { arr: [1, 2, 3, 4, 5] }; + + t.equal( + inspect(nested, { maxArrayLength: 2, depth: 2 }), + '{ arr: [ 1, 2, ... 3 more items ] }', + 'maxArrayLength works with depth option' + ); + + // When depth is exceeded, array shows as [Array] regardless of maxArrayLength + t.equal( + inspect(nested, { maxArrayLength: 2, depth: 1 }), + '{ arr: [Array] }', + 'depth limit shows [Array] regardless of maxArrayLength' + ); + + t.end(); +}); + +test('maxArrayLength with large values', function (t) { + var arr = []; + for (var i = 0; i < 100; i++) { + arr.push(i); + } + + t.equal( + inspect(arr, { maxArrayLength: 1000 }), + inspect(arr), + 'maxArrayLength larger than array length shows all' + ); + + var result = inspect(arr, { maxArrayLength: 5 }); + t.ok( + result.indexOf('... 95 more items') > -1, + 'large array truncated correctly' + ); + + t.end(); +}); + +test('maxArrayLength with objects inside arrays', function (t) { + var arr = [{ a: 1 }, { b: 2 }, { c: 3 }]; + + t.equal( + inspect(arr, { maxArrayLength: 2 }), + '[ { a: 1 }, { b: 2 }, ... 1 more item ]', + 'array of objects truncated at maxArrayLength' + ); + + t.end(); +}); + +test('maxArrayLength with functions inside arrays', function (t) { + function testFn() {} + var arr = [testFn, testFn, testFn]; + + t.equal( + inspect(arr, { maxArrayLength: 1 }), + '[ [Function: testFn], ... 2 more items ]', + 'array of functions truncated at maxArrayLength' + ); + + t.end(); +}); + +test('maxArrayLength does not affect non-array objects', function (t) { + var obj = { a: 1, b: 2, c: 3, d: 4, e: 5 }; + + t.equal( + inspect(obj, { maxArrayLength: 2 }), + '{ a: 1, b: 2, c: 3, d: 4, e: 5 }', + 'maxArrayLength does not affect plain objects' + ); + + t.end(); +}); + +test('maxArrayLength with arguments object', function (t) { + var args = (function () { return arguments; }(1, 2, 3, 4, 5)); + + // arguments objects support maxArrayLength (like node's util.inspect) + t.equal( + inspect(args, { maxArrayLength: 2 }), + 'Arguments [ 1, 2, ... 3 more items ]', + 'arguments object truncated at maxArrayLength' + ); + + t.equal( + inspect(args, { maxArrayLength: 5 }), + 'Arguments [ 1, 2, 3, 4, 5 ]', + 'arguments object not truncated when length equals maxArrayLength' + ); + + t.equal( + inspect(args, { maxArrayLength: 0 }), + 'Arguments [ ... 5 more items ]', + 'arguments object with maxArrayLength 0' + ); + + var emptyArgs = (function () { return arguments; }()); + t.equal( + inspect(emptyArgs, { maxArrayLength: 0 }), + 'Arguments []', + 'empty arguments object' + ); + + t.end(); +}); + +var hasUint8Array = typeof globalThis !== 'undefined' && typeof globalThis.Uint8Array === 'function'; +var hasFloat32Array = typeof globalThis !== 'undefined' && typeof globalThis.Float32Array === 'function'; +var hasInt32Array = typeof globalThis !== 'undefined' && typeof globalThis.Int32Array === 'function'; + +test('maxArrayLength with TypedArrays', { skip: !hasUint8Array }, function (t) { + var Uint8 = globalThis.Uint8Array; + var uint8 = new Uint8([1, 2, 3, 4, 5]); + + t.equal( + inspect(uint8, { maxArrayLength: 3 }), + 'Uint8Array [ 1, 2, 3, ... 2 more items ]', + 'Uint8Array truncated at maxArrayLength' + ); + + t.equal( + inspect(uint8, { maxArrayLength: 5 }), + 'Uint8Array [ 1, 2, 3, 4, 5 ]', + 'Uint8Array not truncated when length equals maxArrayLength' + ); + + t.equal( + inspect(uint8, { maxArrayLength: 0 }), + 'Uint8Array [ ... 5 more items ]', + 'Uint8Array with maxArrayLength 0' + ); + + var emptyUint8 = new Uint8([]); + t.equal( + inspect(emptyUint8, { maxArrayLength: 0 }), + 'Uint8Array []', + 'empty Uint8Array' + ); + + t.end(); +}); + +test('maxArrayLength with Float32Array', { skip: !hasFloat32Array }, function (t) { + var Float32 = globalThis.Float32Array; + var float32 = new Float32([1.5, 2.5, 3.5, 4.5, 5.5]); + + t.equal( + inspect(float32, { maxArrayLength: 2 }), + 'Float32Array [ 1.5, 2.5, ... 3 more items ]', + 'Float32Array truncated at maxArrayLength' + ); + + t.equal( + inspect(float32, { maxArrayLength: 5 }), + 'Float32Array [ 1.5, 2.5, 3.5, 4.5, 5.5 ]', + 'Float32Array not truncated when length equals maxArrayLength' + ); + + t.end(); +}); + +test('maxArrayLength with Int32Array', { skip: !hasInt32Array }, function (t) { + var Int32 = globalThis.Int32Array; + var int32 = new Int32([-1, -2, 3, 4, 5]); + + t.equal( + inspect(int32, { maxArrayLength: 2 }), + 'Int32Array [ -1, -2, ... 3 more items ]', + 'Int32Array truncated at maxArrayLength' + ); + + t.end(); +});