Skip to content

Commit

Permalink
util: add maxItemLength option to truncate iterable entries
Browse files Browse the repository at this point in the history
  • Loading branch information
cola119 committed Jun 29, 2022
1 parent 5629a7c commit 34c3dd4
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 22 deletions.
14 changes: 9 additions & 5 deletions doc/api/util.md
Expand Up @@ -487,7 +487,7 @@ added: v0.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/43576
description: add support for `maxArrayLength` when inspecting `Set` and `Map`.
description: The `maxItemLength` option is supported now.
- version:
- v17.3.0
- v16.14.0
Expand Down Expand Up @@ -588,11 +588,15 @@ changes:
**Default:** `true`.
* `showProxy` {boolean} If `true`, `Proxy` inspection includes
the [`target` and `handler`][] objects. **Default:** `false`.
* `maxArrayLength` {integer} Specifies the maximum number of `Array`,
[`TypedArray`][], [`Map`][], [`Set`][], [`WeakMap`][],
* `maxItemLength` {integer} Specifies the maximum number of iterable items
such as `Array`, [`TypedArray`][], [`Map`][], [`Set`][], [`WeakMap`][],
and [`WeakSet`][] elements to include when formatting.
Set to `null` or `Infinity` to show all elements. Set to `0` or
negative to show no elements. **Default:** `100`.
* `maxArrayLength` {integer} Specifies the maximum number of `Array`,
[`TypedArray`][], [`WeakMap`][], and [`WeakSet`][] elements to include when
formatting. Set to `null` or `Infinity` to show all elements. Set to `0` or
negative to show no elements. **Default:** `100`.
* `maxStringLength` {integer} Specifies the maximum number of characters to
include when formatting. Set to `null` or `Infinity` to show all elements.
Set to `0` or negative to show no characters. **Default:** `10000`.
Expand Down Expand Up @@ -722,7 +726,7 @@ console.log(util.inspect(o, { compact: false, depth: 5, breakLength: 80 }));
```

The `showHidden` option allows [`WeakMap`][] and [`WeakSet`][] entries to be
inspected. If there are more entries than `maxArrayLength`, there is no
inspected. If there are more entries than `maxItemLength`, there is no
guarantee which entries are displayed. That means retrieving the same
[`WeakSet`][] entries twice may result in different output. Furthermore, entries
with no remaining strong references may be garbage collected at any time.
Expand Down Expand Up @@ -1004,7 +1008,7 @@ const util = require('node:util');
const arr = Array(101).fill(0);

console.log(arr); // Logs the truncated array
util.inspect.defaultOptions.maxArrayLength = null;
util.inspect.defaultOptions.maxItemLength = null;
console.log(arr); // logs the full array
```

Expand Down
57 changes: 40 additions & 17 deletions lib/internal/util/inspect.js
Expand Up @@ -159,20 +159,31 @@ const isUndetectableObject = (v) => typeof v === 'undefined' && v !== undefined;

// These options must stay in sync with `getUserOptions`. So if any option will
// be added or removed, `getUserOptions` must also be updated accordingly.
const inspectDefaultOptions = ObjectSeal({
const inspectDefaultOptions = {
showHidden: false,
depth: 2,
colors: false,
customInspect: true,
showProxy: false,
maxArrayLength: 100,
maxItemLength: 100,
maxStringLength: 10000,
breakLength: 80,
compact: 3,
sorted: false,
getters: false,
numericSeparator: false,
};
ObjectDefineProperty(inspectDefaultOptions, 'maxArrayLength', {
__proto__: null,
get() {
return inspectDefaultOptions.maxItemLength;
},
set(val) {
inspectDefaultOptions.maxItemLength = val;
},
enumerable: true,
});
ObjectSeal(inspectDefaultOptions);

const kObjectType = 0;
const kArrayType = 1;
Expand Down Expand Up @@ -242,6 +253,7 @@ function getUserOptions(ctx, isCrossContext) {
customInspect: ctx.customInspect,
showProxy: ctx.showProxy,
maxArrayLength: ctx.maxArrayLength,
maxItemLength: ctx.maxItemLength,
maxStringLength: ctx.maxStringLength,
breakLength: ctx.breakLength,
compact: ctx.compact,
Expand Down Expand Up @@ -302,14 +314,25 @@ function inspect(value, opts) {
colors: inspectDefaultOptions.colors,
customInspect: inspectDefaultOptions.customInspect,
showProxy: inspectDefaultOptions.showProxy,
maxArrayLength: inspectDefaultOptions.maxArrayLength,
maxItemLength: inspectDefaultOptions.maxItemLength,
maxStringLength: inspectDefaultOptions.maxStringLength,
breakLength: inspectDefaultOptions.breakLength,
compact: inspectDefaultOptions.compact,
sorted: inspectDefaultOptions.sorted,
getters: inspectDefaultOptions.getters,
numericSeparator: inspectDefaultOptions.numericSeparator,
};
ObjectDefineProperty(ctx, 'maxArrayLength', {
__proto__: null,
get() {
return ctx.maxItemLength;
},
set(val) {
ctx.maxItemLength = val;
},
enumerable: true,
});

if (arguments.length > 1) {
// Legacy...
if (arguments.length > 2) {
Expand Down Expand Up @@ -342,7 +365,7 @@ function inspect(value, opts) {
}
}
if (ctx.colors) ctx.stylize = stylizeWithColor;
if (ctx.maxArrayLength === null) ctx.maxArrayLength = Infinity;
if (ctx.maxItemLength === null) ctx.maxItemLength = Infinity;
if (ctx.maxStringLength === null) ctx.maxStringLength = Infinity;
return formatValue(ctx, value, 0);
}
Expand Down Expand Up @@ -1345,7 +1368,7 @@ function groupArrayElements(ctx, output, value) {
let maxLength = 0;
let i = 0;
let outputLength = output.length;
if (ctx.maxArrayLength < output.length) {
if (ctx.maxItemLength < output.length) {
// This makes sure the "... n more items" part is not taken into account.
outputLength--;
}
Expand Down Expand Up @@ -1442,7 +1465,7 @@ function groupArrayElements(ctx, output, value) {
}
ArrayPrototypePush(tmp, str);
}
if (ctx.maxArrayLength < output.length) {
if (ctx.maxItemLength < output.length) {
ArrayPrototypePush(tmp, output[outputLength]);
}
output = tmp;
Expand Down Expand Up @@ -1631,17 +1654,17 @@ function formatArrayBuffer(ctx, value) {
hexSlice = uncurryThis(require('buffer').Buffer.prototype.hexSlice);
let str = StringPrototypeTrim(RegExpPrototypeSymbolReplace(
/(.{2})/g,
hexSlice(buffer, 0, MathMin(ctx.maxArrayLength, buffer.length)),
hexSlice(buffer, 0, MathMin(ctx.maxItemLength, buffer.length)),
'$1 '));
const remaining = buffer.length - ctx.maxArrayLength;
const remaining = buffer.length - ctx.maxItemLength;
if (remaining > 0)
str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`;
return [`${ctx.stylize('[Uint8Contents]', 'special')}: <${str}>`];
}

function formatArray(ctx, value, recurseTimes) {
const valLen = value.length;
const len = MathMin(MathMax(0, ctx.maxArrayLength), valLen);
const len = MathMin(MathMax(0, ctx.maxItemLength), valLen);

const remaining = valLen - len;
const output = [];
Expand All @@ -1658,7 +1681,7 @@ function formatArray(ctx, value, recurseTimes) {
}

function formatTypedArray(value, length, ctx, ignored, recurseTimes) {
const maxLength = MathMin(MathMax(0, ctx.maxArrayLength), length);
const maxLength = MathMin(MathMax(0, ctx.maxItemLength), length);
const remaining = value.length - maxLength;
const output = new Array(maxLength);
const elementFormatter = value.length > 0 && typeof value[0] === 'number' ?
Expand Down Expand Up @@ -1691,7 +1714,7 @@ function formatTypedArray(value, length, ctx, ignored, recurseTimes) {

function formatSet(value, ctx, ignored, recurseTimes) {
const length = value.size;
const maxLength = MathMin(MathMax(0, ctx.maxArrayLength), length);
const maxLength = MathMin(MathMax(0, ctx.maxItemLength), length);
const remaining = length - maxLength;
const output = [];
ctx.indentationLvl += 2;
Expand All @@ -1710,7 +1733,7 @@ function formatSet(value, ctx, ignored, recurseTimes) {

function formatMap(value, ctx, ignored, recurseTimes) {
const length = value.size;
const maxLength = MathMin(MathMax(0, ctx.maxArrayLength), length);
const maxLength = MathMin(MathMax(0, ctx.maxItemLength), length);
const remaining = length - maxLength;
const output = [];
ctx.indentationLvl += 2;
Expand All @@ -1730,8 +1753,8 @@ function formatMap(value, ctx, ignored, recurseTimes) {
}

function formatSetIterInner(ctx, recurseTimes, entries, state) {
const maxArrayLength = MathMax(ctx.maxArrayLength, 0);
const maxLength = MathMin(maxArrayLength, entries.length);
const maxItemLength = MathMax(ctx.maxItemLength, 0);
const maxLength = MathMin(maxItemLength, entries.length);
const output = new Array(maxLength);
ctx.indentationLvl += 2;
for (let i = 0; i < maxLength; i++) {
Expand All @@ -1752,11 +1775,11 @@ function formatSetIterInner(ctx, recurseTimes, entries, state) {
}

function formatMapIterInner(ctx, recurseTimes, entries, state) {
const maxArrayLength = MathMax(ctx.maxArrayLength, 0);
const maxItemLength = MathMax(ctx.maxItemLength, 0);
// Entries exist as [key1, val1, key2, val2, ...]
const len = entries.length / 2;
const remaining = len - maxArrayLength;
const maxLength = MathMin(maxArrayLength, len);
const remaining = len - maxItemLength;
const maxLength = MathMin(maxItemLength, len);
let output = new Array(maxLength);
let i = 0;
ctx.indentationLvl += 2;
Expand Down
78 changes: 78 additions & 0 deletions test/parallel/test-util-inspect.js
Expand Up @@ -218,9 +218,15 @@ assert.doesNotMatch(
assert.strictEqual(util.inspect(ab, { showHidden: true, maxArrayLength: 2 }),
'ArrayBuffer { [Uint8Contents]' +
': <00 00 ... 1 more byte>, byteLength: 3 }');
assert.strictEqual(util.inspect(ab, { showHidden: true, maxItemLength: 2 }),
'ArrayBuffer { [Uint8Contents]' +
': <00 00 ... 1 more byte>, byteLength: 3 }');
assert.strictEqual(util.inspect(ab, { showHidden: true, maxArrayLength: 1 }),
'ArrayBuffer { [Uint8Contents]' +
': <00 ... 2 more bytes>, byteLength: 3 }');
assert.strictEqual(util.inspect(ab, { showHidden: true, maxItemLength: 1 }),
'ArrayBuffer { [Uint8Contents]' +
': <00 ... 2 more bytes>, byteLength: 3 }');
}

// Now do the same checks but from a different context.
Expand Down Expand Up @@ -571,10 +577,17 @@ assert.strictEqual(util.inspect(-5e-324), '-5e-324');
util.inspect(a, { maxArrayLength: 4 }),
"[ 'foo', <1 empty item>, 'baz', <97 empty items>, ... 1 more item ]"
);
assert.strictEqual(
util.inspect(a, { maxItemLength: 4 }),
"[ 'foo', <1 empty item>, 'baz', <97 empty items>, ... 1 more item ]"
);
// test 4 special case
assert.strictEqual(util.inspect(a, {
maxArrayLength: 2
}), "[ 'foo', <1 empty item>, ... 99 more items ]");
assert.strictEqual(util.inspect(a, {
maxItemLength: 2
}), "[ 'foo', <1 empty item>, ... 99 more items ]");
}

// Test for Array constructor in different context.
Expand Down Expand Up @@ -1172,6 +1185,7 @@ if (typeof Symbol !== 'undefined') {
assert.strictEqual(util.inspect(new Set()), 'Set(0) {}');
assert.strictEqual(util.inspect(new Set([1, 2, 3])), 'Set(3) { 1, 2, 3 }');
assert.strictEqual(util.inspect(new Set([1, 2, 3]), { maxArrayLength: 1 }), 'Set(3) { 1, ... 2 more items }');
assert.strictEqual(util.inspect(new Set([1, 2, 3]), { maxItemLength: 1 }), 'Set(3) { 1, ... 2 more items }');
const set = new Set(['foo']);
set.bar = 42;
assert.strictEqual(
Expand All @@ -1194,6 +1208,8 @@ if (typeof Symbol !== 'undefined') {
"Map(3) { 1 => 'a', 2 => 'b', 3 => 'c' }");
assert.strictEqual(util.inspect(new Map([[1, 'a'], [2, 'b'], [3, 'c']]), { maxArrayLength: 1 }),
"Map(3) { 1 => 'a', ... 2 more items }");
assert.strictEqual(util.inspect(new Map([[1, 'a'], [2, 'b'], [3, 'c']]), { maxItemLength: 1 }),
"Map(3) { 1 => 'a', ... 2 more items }");
const map = new Map([['foo', null]]);
map.bar = 42;
assert.strictEqual(util.inspect(map, true),
Expand Down Expand Up @@ -1280,6 +1296,8 @@ if (typeof Symbol !== 'undefined') {
map.set('A', 'B!');
assert.strictEqual(util.inspect(map.entries(), { maxArrayLength: 1 }),
"[Map Entries] { [ 'foo', 'bar' ], ... 1 more item }");
assert.strictEqual(util.inspect(map.entries(), { maxItemLength: 1 }),
"[Map Entries] { [ 'foo', 'bar' ], ... 1 more item }");
// Make sure the iterator doesn't get consumed.
const keys = map.keys();
assert.strictEqual(util.inspect(keys), "[Map Iterator] { 'foo', 'A' }");
Expand All @@ -1288,6 +1306,9 @@ if (typeof Symbol !== 'undefined') {
assert.strictEqual(
util.inspect(keys, { maxArrayLength: 0 }),
'[Map Iterator] { ... 2 more items, extra: true }');
assert.strictEqual(
util.inspect(keys, { maxItemLength: 0 }),
'[Map Iterator] { ... 2 more items, extra: true }');
}

// Test Set iterators.
Expand All @@ -1311,6 +1332,9 @@ if (typeof Symbol !== 'undefined') {
assert.strictEqual(
util.inspect(keys, { maxArrayLength: 1 }),
'[Set Iterator] { 1, ... 1 more item, extra: true }');
assert.strictEqual(
util.inspect(keys, { maxItemLength: 1 }),
'[Set Iterator] { 1, ... 1 more item, extra: true }');
}

// Minimal inspection should still return as much information as possible about
Expand Down Expand Up @@ -1511,6 +1535,39 @@ if (typeof Symbol !== 'undefined') {
assert(util.inspect(x, { maxArrayLength: Infinity }).endsWith(' 0, 0\n]'));
}

// maxItemLength
{
const x = new Array(101).fill();
assert(util.inspect(x).endsWith('1 more item\n]'));
assert(!util.inspect(x, { maxItemLength: 101 }).endsWith('1 more item\n]'));
assert.strictEqual(
util.inspect(x, { maxItemLength: -1 }),
'[ ... 101 more items ]'
);
assert.strictEqual(util.inspect(x, { maxItemLength: 0 }),
'[ ... 101 more items ]');
}

{
const x = Array(101);
assert.strictEqual(util.inspect(x, { maxItemLength: 0 }),
'[ ... 101 more items ]');
assert(!util.inspect(x, { maxItemLength: null }).endsWith('1 more item\n]'));
assert(!util.inspect(
x, { maxItemLength: Infinity }
).endsWith('1 more item ]'));
}

{
const x = new Uint8Array(101);
assert(util.inspect(x).endsWith('1 more item\n]'));
assert(!util.inspect(x, { maxItemLength: 101 }).includes('1 more item'));
assert.strictEqual(util.inspect(x, { maxItemLength: 0 }),
'Uint8Array(101) [ ... 101 more items ]');
assert(!util.inspect(x, { maxItemLength: null }).includes('1 more item'));
assert(util.inspect(x, { maxItemLength: Infinity }).endsWith(' 0, 0\n]'));
}

{
const obj = { foo: 'abc', bar: 'xyz' };
const oneLine = util.inspect(obj, { breakLength: Infinity });
Expand Down Expand Up @@ -1922,6 +1979,9 @@ util.inspect(process);
out = util.inspect(weakMap, { maxArrayLength: 0, showHidden: true });
expect = 'WeakMap { ... 2 more items }';
assert.strictEqual(out, expect);
out = util.inspect(weakMap, { maxItemLength: 0, showHidden: true });
expect = 'WeakMap { ... 2 more items }';
assert.strictEqual(out, expect);

weakMap.extra = true;
out = util.inspect(weakMap, { maxArrayLength: 1, showHidden: true });
Expand All @@ -1931,6 +1991,14 @@ util.inspect(process);
'extra: true }';
assert(out === expect || out === expectAlt,
`Found: "${out}"\nrather than: "${expect}"\nor: "${expectAlt}"`);
weakMap.extra = true;
out = util.inspect(weakMap, { maxItemLength: 1, showHidden: true });
// It is not possible to determine the output reliable.
expect = 'WeakMap { [ [length]: 0 ] => {}, ... 1 more item, extra: true }';
expectAlt = 'WeakMap { {} => [ [length]: 0 ], ... 1 more item, ' +
'extra: true }';
assert(out === expect || out === expectAlt,
`Found: "${out}"\nrather than: "${expect}"\nor: "${expectAlt}"`);

// Test WeakSet
arr.push(1);
Expand All @@ -1946,12 +2014,22 @@ util.inspect(process);
out = util.inspect(weakSet, { maxArrayLength: -2, showHidden: true });
expect = 'WeakSet { ... 2 more items }';
assert.strictEqual(out, expect);
out = util.inspect(weakSet, { maxItemLength: -2, showHidden: true });
expect = 'WeakSet { ... 2 more items }';
assert.strictEqual(out, expect);

weakSet.extra = true;
out = util.inspect(weakSet, { maxArrayLength: 1, showHidden: true });
// It is not possible to determine the output reliable.
expect = 'WeakSet { {}, ... 1 more item, extra: true }';
expectAlt = 'WeakSet { [ 1, [length]: 1 ], ... 1 more item, extra: true }';
assert(out === expect || out === expectAlt,
`Found: "${out}"\nrather than: "${expect}"\nor: "${expectAlt}"`);
weakSet.extra = true;
out = util.inspect(weakSet, { maxItemLength: 1, showHidden: true });
// It is not possible to determine the output reliable.
expect = 'WeakSet { {}, ... 1 more item, extra: true }';
expectAlt = 'WeakSet { [ 1, [length]: 1 ], ... 1 more item, extra: true }';
assert(out === expect || out === expectAlt,
`Found: "${out}"\nrather than: "${expect}"\nor: "${expectAlt}"`);
// Keep references to the WeakMap entries, otherwise they could be GCed too
Expand Down

0 comments on commit 34c3dd4

Please sign in to comment.