Repository: nodejs/node
Pull request: util: do not print getters as regular property using inspect

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: #56669

Changed files:
filename: lib/internal/util/inspect.js
patch: @@ -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;

filename: test/parallel/test-util-format.js
patch: @@ -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(

filename: test/parallel/test-util-inspect.js
patch: @@ -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' +