util: mark special properties when inspecting them

This makes sure special properties (such as a byteLength, buffer,
and more) are marked that they are not regular properties. They
are mostly getters, that just seemed even more of a breaking change.
Thus, they just use square brackets for now.

On top of that, it makes inspecting detached DataViews robust.
Inspecting those failed so far.

PR-URL: https://github.com/nodejs/node/pull/60131
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Ruben Bridgewater 2025-10-18 21:29:35 +02:00 committed by GitHub
parent 382f7cfcef
commit 70e7c1511f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 103 additions and 79 deletions

View File

@ -111,7 +111,6 @@ const {
StringPrototypeSplit,
StringPrototypeStartsWith,
StringPrototypeToLowerCase,
StringPrototypeTrim,
StringPrototypeValueOf,
SymbolIterator,
SymbolPrototypeToString,
@ -1223,6 +1222,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
const filter = ctx.showHidden ? ALL_PROPERTIES : ONLY_ENUMERABLE;
let extrasType = kObjectType;
let extraKeys;
// Iterators and the rest are split to reduce checks.
// We have to check all values in case the constructor is set to null.
@ -1278,6 +1278,11 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
// bound function is required to reconstruct missing information.
formatter = FunctionPrototypeBind(formatTypedArray, null, bound, size);
extrasType = kArrayExtrasType;
if (ctx.showHidden) {
extraKeys = ['BYTES_PER_ELEMENT', 'length', 'byteLength', 'byteOffset', 'buffer'];
typedArray = true;
}
} else if (isMapIterator(value)) {
keys = getKeys(value, ctx.showHidden);
braces = getIteratorBraces('Map', tag);
@ -1347,14 +1352,14 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
formatter = formatArrayBuffer;
} else if (keys.length === 0 && protoProps === undefined) {
return prefix +
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength, false)} }`;
`{ [byteLength]: ${formatNumber(ctx.stylize, value.byteLength, false)} }`;
}
braces[0] = `${prefix}{`;
ArrayPrototypeUnshift(keys, 'byteLength');
extraKeys = ['byteLength'];
} else if (isDataView(value)) {
braces[0] = `${getPrefix(constructor, tag, 'DataView')}{`;
// .buffer goes last, it's not a primitive like the others.
ArrayPrototypeUnshift(keys, 'byteLength', 'byteOffset', 'buffer');
extraKeys = ['byteLength', 'byteOffset', 'buffer'];
} else if (isPromise(value)) {
braces[0] = `${getPrefix(constructor, tag, 'Promise')}{`;
formatter = formatPromise;
@ -1404,6 +1409,18 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
const indentationLvl = ctx.indentationLvl;
try {
output = formatter(ctx, value, recurseTimes);
if (extraKeys !== undefined) {
for (i = 0; i < extraKeys.length; i++) {
let formatted;
try {
formatted = formatExtraProperties(ctx, value, recurseTimes, extraKeys[i], typedArray);
} catch {
const tempValue = { [extraKeys[i]]: value.buffer[extraKeys[i]] };
formatted = formatExtraProperties(ctx, tempValue, recurseTimes, extraKeys[i], typedArray);
}
ArrayPrototypePush(output, formatted);
}
}
for (i = 0; i < keys.length; i++) {
ArrayPrototypePush(
output,
@ -2263,11 +2280,15 @@ function formatArrayBuffer(ctx, value) {
}
if (hexSlice === undefined)
hexSlice = uncurryThis(require('buffer').Buffer.prototype.hexSlice);
let str = StringPrototypeTrim(RegExpPrototypeSymbolReplace(
/(.{2})/g,
hexSlice(buffer, 0, MathMin(ctx.maxArrayLength, buffer.length)),
'$1 ',
));
const rawString = hexSlice(buffer, 0, MathMin(ctx.maxArrayLength, buffer.length));
let str = '';
let i = 0;
for (; i < rawString.length - 2; i += 2) {
str += `${rawString[i]}${rawString[i + 1]} `;
}
if (rawString.length > 0) {
str += `${rawString[i]}${rawString[i + 1]}`;
}
const remaining = buffer.length - ctx.maxArrayLength;
if (remaining > 0)
str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`;
@ -2294,7 +2315,7 @@ function formatArray(ctx, value, recurseTimes) {
return output;
}
function formatTypedArray(value, length, ctx, ignored, recurseTimes) {
function formatTypedArray(value, length, ctx) {
const maxLength = MathMin(MathMax(0, ctx.maxArrayLength), length);
const remaining = value.length - maxLength;
const output = new Array(maxLength);
@ -2307,22 +2328,6 @@ function formatTypedArray(value, length, ctx, ignored, recurseTimes) {
if (remaining > 0) {
output[maxLength] = remainingText(remaining);
}
if (ctx.showHidden) {
// .buffer goes last, it's not a primitive like the others.
// All besides `BYTES_PER_ELEMENT` are actually getters.
ctx.indentationLvl += 2;
for (const key of [
'BYTES_PER_ELEMENT',
'length',
'byteLength',
'byteOffset',
'buffer',
]) {
const str = formatValue(ctx, value[key], recurseTimes, true);
ArrayPrototypePush(output, `[${key}]: ${str}`);
}
ctx.indentationLvl -= 2;
}
return output;
}
@ -2470,12 +2475,20 @@ function formatPromise(ctx, value, recurseTimes) {
return output;
}
function formatExtraProperties(ctx, value, recurseTimes, key, typedArray) {
ctx.indentationLvl += 2;
const str = formatValue(ctx, value[key], recurseTimes, typedArray);
ctx.indentationLvl -= 2;
// These entries are mainly getters. Should they be formatted like getters?
return ctx.stylize(`[${key}]: ${str}`, 'string');
}
function formatProperty(ctx, value, recurseTimes, key, type, desc,
original = value) {
let name, str;
let extra = ' ';
desc ||= ObjectGetOwnPropertyDescriptor(value, key) ||
{ value: value[key], enumerable: true };
desc ??= ObjectGetOwnPropertyDescriptor(value, key);
if (desc.value !== undefined) {
const diff = (ctx.compact !== true || type !== kObjectType) ? 2 : 3;
ctx.indentationLvl += diff;

View File

@ -588,7 +588,7 @@ assert.strictEqual(
assert.strictEqual(
util.format(new SharedArrayBuffer(4)),
'SharedArrayBuffer { [Uint8Contents]: <00 00 00 00>, byteLength: 4 }'
'SharedArrayBuffer { [Uint8Contents]: <00 00 00 00>, [byteLength]: 4 }'
);
assert.strictEqual(

View File

@ -172,56 +172,67 @@ assert.doesNotMatch(
const dv = new DataView(ab, 1, 2);
assert.strictEqual(
util.inspect(ab, showHidden),
'ArrayBuffer { [Uint8Contents]: <01 02 03 04>, byteLength: 4 }'
'ArrayBuffer { [Uint8Contents]: <01 02 03 04>, [byteLength]: 4 }'
);
assert.strictEqual(util.inspect(new DataView(ab, 1, 2), showHidden),
'DataView {\n' +
' byteLength: 2,\n' +
' byteOffset: 1,\n' +
' buffer: ArrayBuffer {' +
' [Uint8Contents]: <01 02 03 04>, byteLength: 4 }\n}');
' [byteLength]: 2,\n' +
' [byteOffset]: 1,\n' +
' [buffer]: ArrayBuffer {' +
' [Uint8Contents]: <01 02 03 04>, [byteLength]: 4 }\n}');
assert.strictEqual(
util.inspect(ab, showHidden),
'ArrayBuffer { [Uint8Contents]: <01 02 03 04>, byteLength: 4 }'
'ArrayBuffer { [Uint8Contents]: <01 02 03 04>, [byteLength]: 4 }'
);
assert.strictEqual(util.inspect(dv, showHidden),
'DataView {\n' +
' byteLength: 2,\n' +
' byteOffset: 1,\n' +
' buffer: ArrayBuffer { [Uint8Contents]: ' +
'<01 02 03 04>, byteLength: 4 }\n}');
' [byteLength]: 2,\n' +
' [byteOffset]: 1,\n' +
' [buffer]: ArrayBuffer { [Uint8Contents]: ' +
'<01 02 03 04>, [byteLength]: 4 }\n}');
ab.x = 42;
dv.y = 1337;
assert.strictEqual(util.inspect(ab, showHidden),
'ArrayBuffer { [Uint8Contents]: <01 02 03 04>, ' +
'byteLength: 4, x: 42 }');
assert.strictEqual(util.inspect(dv, showHidden),
'[byteLength]: 4, x: 42 }');
assert.strictEqual(util.inspect(dv, { showHidden, breakLength: 82 }),
'DataView {\n' +
' byteLength: 2,\n' +
' byteOffset: 1,\n' +
' buffer: ArrayBuffer { [Uint8Contents]: <01 02 03 04>,' +
' byteLength: 4, x: 42 },\n' +
' [byteLength]: 2,\n' +
' [byteOffset]: 1,\n' +
' [buffer]: ArrayBuffer { [Uint8Contents]: <01 02 03 04>,' +
' [byteLength]: 4, x: 42 },\n' +
' y: 1337\n}');
}
{
const ab = new ArrayBuffer(42);
const dv = new DataView(ab);
assert.strictEqual(ab.byteLength, 42);
new MessageChannel().port1.postMessage(ab, [ ab ]);
assert.strictEqual(ab.byteLength, 0);
assert.strictEqual(util.inspect(ab),
'ArrayBuffer { (detached), byteLength: 0 }');
'ArrayBuffer { (detached), [byteLength]: 0 }');
assert.strictEqual(
util.inspect(dv),
'DataView {\n' +
' [byteLength]: 0,\n' +
' [byteOffset]: undefined,\n' +
' [buffer]: ArrayBuffer { (detached), [byteLength]: 0 }\n' +
' }',
);
}
// Truncate output for ArrayBuffers using plural or singular bytes
{
const ab = new ArrayBuffer(3);
assert.strictEqual(util.inspect(ab, { showHidden: true, maxArrayLength: 2 }),
assert.strictEqual(util.inspect(ab, { showHidden: true, maxArrayLength: 2, breakLength: 82 }),
'ArrayBuffer { [Uint8Contents]' +
': <00 00 ... 1 more byte>, byteLength: 3 }');
assert.strictEqual(util.inspect(ab, { showHidden: true, maxArrayLength: 1 }),
': <00 00 ... 1 more byte>, [byteLength]: 3 }');
assert.strictEqual(util.inspect(ab, { showHidden: true, maxArrayLength: 1, breakLength: 82 }),
'ArrayBuffer { [Uint8Contents]' +
': <00 ... 2 more bytes>, byteLength: 3 }');
': <00 ... 2 more bytes>, [byteLength]: 3 }');
}
// Now do the same checks but from a different context.
@ -231,35 +242,35 @@ assert.doesNotMatch(
const dv = vm.runInNewContext('new DataView(ab, 1, 2)', { ab });
assert.strictEqual(
util.inspect(ab, showHidden),
'ArrayBuffer { [Uint8Contents]: <00 00 00 00>, byteLength: 4 }'
'ArrayBuffer { [Uint8Contents]: <00 00 00 00>, [byteLength]: 4 }'
);
assert.strictEqual(util.inspect(new DataView(ab, 1, 2), showHidden),
'DataView {\n' +
' byteLength: 2,\n' +
' byteOffset: 1,\n' +
' buffer: ArrayBuffer { [Uint8Contents]: <00 00 00 00>,' +
' byteLength: 4 }\n}');
' [byteLength]: 2,\n' +
' [byteOffset]: 1,\n' +
' [buffer]: ArrayBuffer { [Uint8Contents]: <00 00 00 00>,' +
' [byteLength]: 4 }\n}');
assert.strictEqual(
util.inspect(ab, showHidden),
'ArrayBuffer { [Uint8Contents]: <00 00 00 00>, byteLength: 4 }'
'ArrayBuffer { [Uint8Contents]: <00 00 00 00>, [byteLength]: 4 }'
);
assert.strictEqual(util.inspect(dv, showHidden),
'DataView {\n' +
' byteLength: 2,\n' +
' byteOffset: 1,\n' +
' buffer: ArrayBuffer { [Uint8Contents]: <00 00 00 00>,' +
' byteLength: 4 }\n}');
' [byteLength]: 2,\n' +
' [byteOffset]: 1,\n' +
' [buffer]: ArrayBuffer { [Uint8Contents]: <00 00 00 00>,' +
' [byteLength]: 4 }\n}');
ab.x = 42;
dv.y = 1337;
assert.strictEqual(util.inspect(ab, showHidden),
'ArrayBuffer { [Uint8Contents]: <00 00 00 00>, ' +
'byteLength: 4, x: 42 }');
assert.strictEqual(util.inspect(dv, showHidden),
'[byteLength]: 4, x: 42 }');
assert.strictEqual(util.inspect(dv, { showHidden, breakLength: 82 }),
'DataView {\n' +
' byteLength: 2,\n' +
' byteOffset: 1,\n' +
' buffer: ArrayBuffer { [Uint8Contents]: <00 00 00 00>,' +
' byteLength: 4, x: 42 },\n' +
' [byteLength]: 2,\n' +
' [byteOffset]: 1,\n' +
' [buffer]: ArrayBuffer { [Uint8Contents]: <00 00 00 00>,' +
' [byteLength]: 4, x: 42 },\n' +
' y: 1337\n}');
}
@ -286,7 +297,7 @@ assert.doesNotMatch(
` [length]: ${length},\n` +
` [byteLength]: ${byteLength},\n` +
' [byteOffset]: 0,\n' +
` [buffer]: ArrayBuffer { byteLength: ${byteLength} }\n]`);
` [buffer]: ArrayBuffer { [byteLength]: ${byteLength} }\n]`);
assert.strictEqual(
util.inspect(array, false),
`${constructor.name}(${length}) [ 65, 97 ]`
@ -320,7 +331,7 @@ assert.doesNotMatch(
` [length]: ${length},\n` +
` [byteLength]: ${byteLength},\n` +
' [byteOffset]: 0,\n' +
` [buffer]: ArrayBuffer { byteLength: ${byteLength} }\n]`);
` [buffer]: ArrayBuffer { [byteLength]: ${byteLength} }\n]`);
assert.strictEqual(
util.inspect(array, false),
`${constructor.name}(${length}) [ 65, 97 ]`
@ -1837,7 +1848,7 @@ util.inspect(process);
' [byteLength]: 0,',
' [byteOffset]: 0,',
' [buffer]: ArrayBuffer {',
' byteLength: 0,',
' [byteLength]: 0,',
' foo: true',
' }',
' ],',
@ -1855,7 +1866,7 @@ util.inspect(process);
' [byteLength]: 0,',
' [byteOffset]: 0,',
' [buffer]: ArrayBuffer {',
' byteLength: 0,',
' [byteLength]: 0,',
' foo: true',
' }',
' ],',
@ -1885,7 +1896,7 @@ util.inspect(process);
' [length]: 0,',
' [byteLength]: 0,',
' [byteOffset]: 0,',
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
' [buffer]: ArrayBuffer { [byteLength]: 0, foo: true }',
' ],',
' [Set Iterator] {',
' [ 1, 2, [length]: 2 ],',
@ -1896,7 +1907,7 @@ util.inspect(process);
' [length]: 0,',
' [byteLength]: 0,',
' [byteOffset]: 0,',
' [buffer]: ArrayBuffer { byteLength: 0, foo: true }',
' [buffer]: ArrayBuffer { [byteLength]: 0, foo: true }',
' ],',
' [Circular *1],',
" [Symbol(Symbol.toStringTag)]: 'Map Iterator'",
@ -1924,7 +1935,7 @@ util.inspect(process);
' [byteLength]: 0,',
' [byteOffset]: 0,',
' [buffer]: ArrayBuffer {',
' byteLength: 0,',
' [byteLength]: 0,',
' foo: true } ],',
' [Set Iterator] {',
' [ 1,',
@ -1938,7 +1949,7 @@ util.inspect(process);
' [byteLength]: 0,',
' [byteOffset]: 0,',
' [buffer]: ArrayBuffer {',
' byteLength: 0,',
' [byteLength]: 0,',
' foo: true } ],',
' [Circular *1],',
' [Symbol(Symbol.toStringTag)]:',
@ -2244,12 +2255,12 @@ assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");
[new BigUint64Array(2), '[BigUint64Array(2): null prototype] [ 0n, 0n ]'],
[new ArrayBuffer(16), '[ArrayBuffer: null prototype] {\n' +
' [Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>,\n' +
' byteLength: undefined\n}'],
' [byteLength]: undefined\n}'],
[new DataView(new ArrayBuffer(16)),
'[DataView: null prototype] {\n byteLength: undefined,\n ' +
'byteOffset: undefined,\n buffer: undefined\n}'],
'[DataView: null prototype] {\n [byteLength]: undefined,\n ' +
'[byteOffset]: undefined,\n [buffer]: undefined\n}'],
[new SharedArrayBuffer(2), '[SharedArrayBuffer: null prototype] ' +
'{\n [Uint8Contents]: <00 00>,\n byteLength: undefined\n}'],
'{\n [Uint8Contents]: <00 00>,\n [byteLength]: undefined\n}'],
[/foobar/, '[RegExp: null prototype] /foobar/'],
[new Date('Sun, 14 Feb 2010 11:48:40 GMT'),
'[Date: null prototype] 2010-02-14T11:48:40.000Z'],
@ -3632,7 +3643,7 @@ assert.strictEqual(
assert.strictEqual(
util.inspect(o),
'{\n' +
' arrayBuffer: ArrayBuffer { [Uint8Contents]: <>, byteLength: 0 },\n' +
' arrayBuffer: ArrayBuffer { [Uint8Contents]: <>, [byteLength]: 0 },\n' +
' buffer: <Buffer 48 65 6c 6c 6f>,\n' +
' typedArray: TypedArray(5) [Uint8Array] [ 72, 101, 108, 108, 111 ],\n' +
' array: [],\n' +