Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions benchmark/util/inspect-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';

const common = require('../common');
const util = require('util');

const bench = common.createBenchmark(main, {
n: [1e3],
len: [1e2, 1e4],
maxObjectProperties: [0, 10, 100, Infinity],
showHidden: [0, 1],
});

function main({ n, len, maxObjectProperties, showHidden }) {
const prototype = {};
const object = { __proto__: prototype };
for (let i = 0; i < len; i++) {
object[`property${i}`] = { value: i };
prototype[`prototypeProperty${i}`] = { value: i };
}

const options = {
maxObjectProperties,
showHidden: showHidden === 1,
};

bench.start();
for (let i = 0; i < n; i++) {
util.inspect(object, options);
}
bench.end(n);
}
11 changes: 11 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,9 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65242
description: The `maxObjectProperties` option is supported now.
- version:
- v25.0.0
pr-url: https://github.com/nodejs/node/pull/59710
Expand Down Expand Up @@ -1023,6 +1026,14 @@ changes:
{TypedArray}, {Map}, {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`.
* `maxObjectProperties` {integer} Specifies the maximum number of named
properties per inspected value. Own string and symbol properties are
included before user-defined prototype properties. Content governed by
`maxArrayLength` and built-in metadata entries such as `[byteLength]`,
`[buffer]`, and `[BYTES_PER_ELEMENT]` do not count. The limit is applied
before `sorted`.
Set to `null` or `Infinity` to show all properties. Set to `0` or negative
to show no properties. **Default:** `Infinity`.
* `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
1 change: 1 addition & 0 deletions lib/internal/assert/assertion_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ function inspectValue(val) {
customInspect: false,
depth: 1000,
maxArrayLength: Infinity,
maxObjectProperties: Infinity,
// Assert compares only enumerable properties (with a few exceptions).
showHidden: false,
// Assert does not detect proxies currently.
Expand Down
115 changes: 83 additions & 32 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const {
ArrayPrototypeIndexOf,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeSlice,
Expand Down Expand Up @@ -227,6 +226,7 @@ const inspectDefaultOptions = ObjectSeal({
customInspect: true,
showProxy: false,
maxArrayLength: 100,
maxObjectProperties: Infinity,
maxStringLength: 10000,
breakLength: 80,
compact: 3,
Expand Down Expand Up @@ -305,6 +305,7 @@ function getUserOptions(ctx, isCrossContext) {
customInspect: ctx.customInspect,
showProxy: ctx.showProxy,
maxArrayLength: ctx.maxArrayLength,
maxObjectProperties: ctx.maxObjectProperties,
maxStringLength: ctx.maxStringLength,
breakLength: ctx.breakLength,
compact: ctx.compact,
Expand Down Expand Up @@ -365,6 +366,7 @@ function inspect(value, opts) {
customInspect: inspectDefaultOptions.customInspect,
showProxy: inspectDefaultOptions.showProxy,
maxArrayLength: inspectDefaultOptions.maxArrayLength,
maxObjectProperties: inspectDefaultOptions.maxObjectProperties,
maxStringLength: inspectDefaultOptions.maxStringLength,
breakLength: inspectDefaultOptions.breakLength,
compact: inspectDefaultOptions.compact,
Expand Down Expand Up @@ -405,6 +407,7 @@ function inspect(value, opts) {
}
if (ctx.colors) ctx.stylize = stylizeWithColor;
if (ctx.maxArrayLength === null) ctx.maxArrayLength = Infinity;
if (ctx.maxObjectProperties === null) ctx.maxObjectProperties = Infinity;
if (ctx.maxStringLength === null) ctx.maxStringLength = Infinity;
return formatValue(ctx, value, 0);
}
Expand Down Expand Up @@ -914,8 +917,7 @@ function getConstructorName(obj, ctx, recurseTimes, protoProps) {
const { name, constructor } = wellKnownPrototypeNameAndConstructor;
if (FunctionPrototypeSymbolHasInstance(constructor, tmp)) {
if (protoProps !== undefined && firstProto !== obj) {
addPrototypeProperties(
ctx, tmp, firstProto || tmp, recurseTimes, protoProps);
addPrototypeProperties(tmp, firstProto || tmp, protoProps);
}
return name;
}
Expand All @@ -928,8 +930,7 @@ function getConstructorName(obj, ctx, recurseTimes, protoProps) {
if (protoProps !== undefined &&
(firstProto !== obj ||
!builtInObjects.has(descriptor.value.name))) {
addPrototypeProperties(
ctx, tmp, firstProto || tmp, recurseTimes, protoProps);
addPrototypeProperties(tmp, firstProto || tmp, protoProps);
}
return String(descriptor.value.name);
}
Expand Down Expand Up @@ -964,10 +965,9 @@ function getConstructorName(obj, ctx, recurseTimes, protoProps) {
return `${res} <${protoConstr}>`;
}

// This function has the side effect of adding prototype properties to the
// `output` argument (which is an array). This is intended to highlight user
// defined prototype properties.
function addPrototypeProperties(ctx, main, obj, recurseTimes, output) {
// Collect user defined prototype properties. They are formatted later so a
// property limit can discard them without inspecting their values.
function addPrototypeProperties(main, obj, output) {
let depth = 0;
let keys;
let keySet;
Expand All @@ -994,7 +994,6 @@ function addPrototypeProperties(ctx, main, obj, recurseTimes, output) {
}
// Get all own property names and symbols.
keys = ReflectOwnKeys(obj);
ArrayPrototypePush(ctx.seen, main);
for (const key of keys) {
// Ignore the `constructor` property and keys that exist on layers above.
if (key === 'constructor' ||
Expand All @@ -1006,22 +1005,41 @@ function addPrototypeProperties(ctx, main, obj, recurseTimes, output) {
if (typeof desc.value === 'function') {
continue;
}
const value = formatProperty(
ctx, obj, recurseTimes, key, kObjectType, desc, main);
if (ctx.colors) {
// Faint!
ArrayPrototypePush(output, `\u001b[2m${value}\u001b[22m`);
} else {
ArrayPrototypePush(output, value);
}
ArrayPrototypePush(output, [obj, key, desc]);
}
ArrayPrototypePop(ctx.seen);
// Limit the inspection to up to three prototype layers. Using `recurseTimes`
// is not a good choice here, because it's as if the properties are declared
// on the current object from the users perspective.
} while (++depth !== 3);
}

function truncateProperties(properties, limit) {
// A NaN limit leaves `properties` untouched.
if (!(properties.length > limit)) {
return 0;
}
const omitted = properties.length - limit;
properties.length = limit;
return omitted;
}

// Discards properties beyond `ctx.maxObjectProperties`, retaining own
// properties before prototype properties. Property candidates have already
// been collected, so this limits formatting rather than enumeration. Returns
// the number omitted.
function limitProperties(ctx, keys, protoProps) {
if (ctx.maxObjectProperties === Infinity) {
return 0;
}
// Assigning to `array.length` requires an integral value.
const budget = MathTrunc(MathMax(0, ctx.maxObjectProperties));
let omitted = truncateProperties(keys, budget);
if (protoProps !== undefined) {
omitted += truncateProperties(protoProps, budget - keys.length);
}
return omitted;
}

/** @type {(constructor: string, tag: string, fallback: string, size?: string) => string} */
function getPrefix(constructor, tag, fallback, size = '') {
if (constructor === null) {
Expand Down Expand Up @@ -1422,9 +1440,26 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
constructorName = `[${constructorName}]`;
return ctx.stylize(constructorName, 'special');
}

// This must run after type-specific key normalization and empty-value fast
// paths. It must not move into getKeys().
const remainingProperties = limitProperties(ctx, keys, protoProps);

const protoRecurseTimes = recurseTimes;
recurseTimes += 1;

ctx.seen.push(value);
let formattedProtoProps;
if (protoProps !== undefined) {
formattedProtoProps = new Array(protoProps.length);
for (i = 0; i < protoProps.length; i++) {
const { 0: obj, 1: key, 2: desc } = protoProps[i];
const formatted = formatProperty(
ctx, obj, protoRecurseTimes, key, kObjectType, desc, value);
formattedProtoProps[i] =
ctx.colors ? `\u001b[2m${formatted}\u001b[22m` : formatted;
}
}
ctx.currentDepth = recurseTimes;
let output;
const indentationLvl = ctx.indentationLvl;
Expand All @@ -1448,8 +1483,8 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
formatProperty(ctx, value, recurseTimes, keys[i], extrasType),
);
}
if (protoProps !== undefined) {
ArrayPrototypePushApply(output, protoProps);
if (formattedProtoProps !== undefined) {
ArrayPrototypePushApply(output, formattedProtoProps);
}
} catch (err) {
if (!isStackOverflowError(err)) throw err;
Expand Down Expand Up @@ -1480,9 +1515,16 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
ReflectApply(ArrayPrototypeSplice, null, sorted);
}
}
if (remainingProperties > 0) {
ArrayPrototypePush(
output,
remainingText(remainingProperties, 'property', 'properties'),
);
}

const res = reduceToSingleString(
ctx, output, base, braces, extrasType, recurseTimes, value);
ctx, output, base, braces, extrasType, recurseTimes, value,
remainingProperties > 0 ? 1 : 0);
const budget = ctx.budget[ctx.indentationLvl] || 0;
const newLength = budget + res.length;
ctx.budget[ctx.indentationLvl] = newLength;
Expand Down Expand Up @@ -2048,12 +2090,15 @@ function formatError(err, constructor, tag, ctx, keys) {
return stack;
}

function groupArrayElements(ctx, output, value) {
// `trailingEntries` counts entries at the end of `output` that describe omitted
// content rather than real entries. They must not take part in grouping.
function groupArrayElements(ctx, output, value, trailingEntries) {
let totalLength = 0;
let maxLength = 0;
let i = 0;
let outputLength = output.length;
if (ctx.maxArrayLength < output.length) {
const outputLengthWithoutTrailing = output.length - trailingEntries;
let outputLength = outputLengthWithoutTrailing;
if (ctx.maxArrayLength < outputLengthWithoutTrailing) {
// This makes sure the "... n more items" part is not taken into account.
outputLength--;
}
Expand All @@ -2080,7 +2125,8 @@ function groupArrayElements(ctx, output, value) {
(totalLength / actualMax > 5 || maxLength <= 6)) {

const approxCharHeights = 2.5;
const averageBias = MathSqrt(actualMax - totalLength / output.length);
const averageBias =
MathSqrt(actualMax - totalLength / outputLengthWithoutTrailing);
const biasedMax = MathMax(actualMax - 3 - averageBias, 1);
// Dynamically check how many columns seem possible.
const columns = MathMin(
Expand Down Expand Up @@ -2110,7 +2156,7 @@ function groupArrayElements(ctx, output, value) {
const maxLineLength = [];
for (let i = 0; i < columns; i++) {
let lineMaxLength = 0;
for (let j = i; j < output.length; j += columns) {
for (let j = i; j < outputLengthWithoutTrailing; j += columns) {
if (dataLen[j] > lineMaxLength)
lineMaxLength = dataLen[j];
}
Expand All @@ -2119,7 +2165,7 @@ function groupArrayElements(ctx, output, value) {
}
let order = StringPrototypePadStart;
if (value !== undefined) {
for (let i = 0; i < output.length; i++) {
for (let i = 0; i < outputLengthWithoutTrailing; i++) {
if (typeof value[i] !== 'number' && typeof value[i] !== 'bigint') {
order = StringPrototypePadEnd;
break;
Expand Down Expand Up @@ -2150,9 +2196,12 @@ function groupArrayElements(ctx, output, value) {
}
ArrayPrototypePush(tmp, str);
}
if (ctx.maxArrayLength < output.length) {
if (ctx.maxArrayLength < outputLengthWithoutTrailing) {
ArrayPrototypePush(tmp, output[outputLength]);
}
for (let i = outputLengthWithoutTrailing; i < output.length; i++) {
ArrayPrototypePush(tmp, output[i]);
}
output = tmp;
}
return output;
Expand Down Expand Up @@ -2192,7 +2241,8 @@ function addNumericSeparatorEnd(integerString) {
`${result}${StringPrototypeSlice(integerString, i)}`;
}

const remainingText = (remaining) => `... ${remaining} more item${remaining > 1 ? 's' : ''}`;
const remainingText = (remaining, singular = 'item', plural = 'items') =>
`... ${remaining} more ${remaining > 1 ? plural : singular}`;

function formatNumber(fn, number, numericSeparator) {
// Format -0 as '-0'. Checking `number === -0` won't distinguish 0 from -0.
Expand Down Expand Up @@ -2642,7 +2692,8 @@ function isBelowBreakLength(ctx, output, start, base) {
}

function reduceToSingleString(
ctx, output, base, braces, extrasType, recurseTimes, value) {
ctx, output, base, braces, extrasType, recurseTimes, value,
trailingEntries = 0) {
if (ctx.compact !== true) {
if (typeof ctx.compact === 'number' && ctx.compact >= 1) {
// Memorize the original output length. In case the output is grouped,
Expand All @@ -2651,7 +2702,7 @@ function reduceToSingleString(
// Group array elements together if the array contains at least six
// separate entries.
if (extrasType === kArrayExtrasType && entries > 6) {
output = groupArrayElements(ctx, output, value);
output = groupArrayElements(ctx, output, value, trailingEntries);
}
// `ctx.currentDepth` is set to the most inner depth of the currently
// inspected object part while `recurseTimes` is the actual current depth
Expand Down
20 changes: 20 additions & 0 deletions test/parallel/test-assert-deep.js
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,26 @@ test('Check proxies', () => {
);
});

test('Assertion errors ignore the maxObjectProperties default', () => {
const original = util.inspect.defaultOptions.maxObjectProperties;
util.inspect.defaultOptions.maxObjectProperties = 1;
try {
assert.throws(
() => assert.deepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 3 }),
{
message: `${defaultMsgStartFull}\n\n` +
' {\n' +
' a: 1,\n' +
'+ b: 2\n' +
'- b: 3\n' +
' }\n'
}
);
} finally {
util.inspect.defaultOptions.maxObjectProperties = original;
}
});

test('Strict equal with identical objects that are not identical ' +
'by reference and longer than 50 elements', () => {
// E.g., assert.deepStrictEqual({ a: Symbol() }, { a: Symbol() })
Expand Down
6 changes: 6 additions & 0 deletions test/parallel/test-repl-inspect-defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ child.on('exit', common.mustCall(() => {
results,
[
'[ 42, 23 ]',
'{ first: 1, second: 2 }',
'1',
'{ first: 1, ... 1 more property }',
'1',
'[ 42, ... 1 more item ]',
'',
Expand All @@ -25,6 +28,9 @@ child.on('exit', common.mustCall(() => {
}));

child.stdin.write('[ 42, 23 ]\n');
child.stdin.write('({ first: 1, second: 2 })\n');
child.stdin.write('util.inspect.replDefaults.maxObjectProperties = 1\n');
child.stdin.write('({ first: 1, second: 2 })\n');
child.stdin.write('util.inspect.replDefaults.maxArrayLength = 1\n');
child.stdin.write('[ 42, 23 ]\n');
child.stdin.end();
Loading
Loading