From 6599727151d1ef231cd0b127ce27059ada496447 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 6 Oct 2025 16:40:48 +0200 Subject: [PATCH 1/2] 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. Closes: https://github.com/nodejs/node/pull/56669 --- lib/internal/util/inspect.js | 69 ++++++++++-------- test/parallel/test-util-inspect.js | 111 ++++++++++++++++------------- 2 files changed, 102 insertions(+), 78 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index d27b7df778f100..fac1e406acdd4d 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -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; diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 89b798f20cba6d..8e19ba3fe9dca9 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -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: ,\n' + ' typedArray: TypedArray(5) [Uint8Array] [ 72, 101, 108, 108, 111 ],\n' + ' array: [],\n' + From db6203accc1ccc09246369413b635518c9215d2e Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 6 Oct 2025 17:31:20 +0200 Subject: [PATCH 2/2] fixup! --- test/parallel/test-util-format.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-util-format.js b/test/parallel/test-util-format.js index ad77c7cafd38fb..dfcf62c378b84b 100644 --- a/test/parallel/test-util-format.js +++ b/test/parallel/test-util-format.js @@ -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(