diff --git a/src/js/internal/util/inspect.js b/src/js/internal/util/inspect.js index 09d55ecafcbe..f09e24798e05 100644 --- a/src/js/internal/util/inspect.js +++ b/src/js/internal/util/inspect.js @@ -344,12 +344,20 @@ function isURL(value) { const SymbolToPrimitive = Symbol.toPrimitive; -const builtInObjects = new SafeSet( - ArrayPrototypeFilter( - ObjectGetOwnPropertyNames(globalThis), - e => RegExpPrototypeExec(/^[A-Z][a-zA-Z0-9]+$/, e) !== null, - ), -); +// Node.js computes this from `globalThis` at bootstrap, before any host globals +// (Buffer, URL, ...) are installed. Bun's globalThis already has them when this +// module loads, so hardcode the names Node observes instead of scraping. +// prettier-ignore +const builtInObjects = new SafeSet([ + "AggregateError", "Array", "ArrayBuffer", "Atomics", "BigInt", "BigInt64Array", + "BigUint64Array", "Boolean", "DataView", "Date", "Error", "EvalError", + "FinalizationRegistry", "Float32Array", "Float64Array", "Function", "Infinity", + "Int16Array", "Int32Array", "Int8Array", "Intl", "Iterator", "JSON", "Map", + "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", + "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "SyntaxError", + "TypeError", "URIError", "Uint16Array", "Uint32Array", "Uint8Array", + "Uint8ClampedArray", "WeakMap", "WeakRef", "WeakSet", +]); // https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot const isUndetectableObject = v => typeof v === "undefined" && v !== undefined; diff --git a/test/js/node/util/node-inspect-tests/parallel/util-format.test.js b/test/js/node/util/node-inspect-tests/parallel/util-format.test.js index 57bbac75fb0c..bf822bf6d0be 100644 --- a/test/js/node/util/node-inspect-tests/parallel/util-format.test.js +++ b/test/js/node/util/node-inspect-tests/parallel/util-format.test.js @@ -238,6 +238,25 @@ test("no assertion failures", () => { assert.strictEqual(util.format("%s", { __proto__: null }), "[Object: null prototype] {}"); } + // `%s` with values whose inherited `toString` comes from a non-ECMAScript + // built-in (Buffer, URL, ...) must call `String(value)`, not inspect it. + assert.strictEqual(util.format("%s", Buffer.from("ab")), "ab"); + assert.strictEqual(util.format("%s", Buffer.from([0xe2, 0x82, 0xac])), "\u20ac"); + assert.strictEqual(util.format("%s", new URL("http://a/b")), "http://a/b"); + assert.strictEqual(util.format("%s", new URLSearchParams("a=1&b=2")), "a=1&b=2"); + { + // Subclass path: toString is inherited through an extra prototype hop. + class MyURL extends URL {} + assert.strictEqual(util.format("%s", new MyURL("http://a/b")), "http://a/b"); + class MyBuffer extends Buffer {} + const sub = Object.setPrototypeOf(Buffer.from([0x68, 0x69]), MyBuffer.prototype); + assert.strictEqual(util.format("%s", sub), "hi"); + } + // Uint8Array inherits toString from %TypedArray%.prototype, not Buffer. + assert.strictEqual(util.format("%s", new Uint8Array([65])), "65"); + // ECMAScript built-ins with their own toString still take the inspect path. + assert.strictEqual(util.format("%s", [1, 2]), "[ 1, 2 ]"); + // JSON format specifier assert.strictEqual(util.format("%j"), "%j"); assert.strictEqual(util.format("%j", 42), "42"); diff --git a/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js b/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js index cc88c7ff7367..3074ea6300e9 100644 --- a/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js +++ b/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js @@ -20,7 +20,7 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. import assert from "assert"; -import { isWindows } from "harness"; +import { isDebug, isWindows } from "harness"; import util, { inspect } from "util"; import vm from "vm"; import { MessageChannel } from "worker_threads"; @@ -238,7 +238,6 @@ test("inspect from a different context", () => { test("no assertion failures 2", () => { [ - Float16Array, Float32Array, Float64Array, Int16Array, @@ -268,9 +267,17 @@ test("no assertion failures 2", () => { assert.strictEqual(util.inspect(array, false), `${constructor.name}(${length}) [ 65, 97 ]`); }); + // Float16Array is absent from Node's bootstrap-time `builtInObjects`, so + // showHidden walks into its prototype and the output diverges from the + // other typed arrays. Lock that in so it is not silently re-added. + assert.match( + util.inspect(new Float16Array(2), { showHidden: true }), + /\[Symbol\(Symbol\.toStringTag\)\]: \[Getter\]/, + ); + assert.strictEqual(util.inspect(new Float16Array([65, 97]), false), "Float16Array(2) [ 65, 97 ]"); + // Now check that declaring a TypedArray in a different context works the same. [ - Float16Array, Float32Array, Float64Array, Int16Array, @@ -775,39 +782,6 @@ test("no assertion failures 2", () => { assert.strictEqual(util.inspect(y), "[ 'a', 'b', 'c', '\\\\\\\\': 'd', " + "'\\n': 'e', '\\r': 'f' ]"); } - // Escape unpaired surrogate pairs. - { - const edgeChar = String.fromCharCode(0xd799); - - for (let charCode = 0xd800; charCode < 0xdfff; charCode++) { - const surrogate = String.fromCharCode(charCode); - - assert.strictEqual(util.inspect(surrogate), `'\\u${charCode.toString(16)}'`); - assert.strictEqual( - util.inspect(`${"a".repeat(200)}${surrogate}`), - `'${"a".repeat(200)}\\u${charCode.toString(16)}'`, - ); - assert.strictEqual( - util.inspect(`${surrogate}${"a".repeat(200)}`), - `'\\u${charCode.toString(16)}${"a".repeat(200)}'`, - ); - if (charCode < 0xdc00) { - const highSurrogate = surrogate; - const lowSurrogate = String.fromCharCode(charCode + 1024); - assert(!util.inspect(`${edgeChar}${highSurrogate}${lowSurrogate}${edgeChar}`).includes("\\u")); - assert.strictEqual( - (util.inspect(`${highSurrogate}${highSurrogate}${lowSurrogate}`).match(/\\u/g) ?? []).length, - 1, - ); - } else { - assert.strictEqual( - util.inspect(`${edgeChar}${surrogate}${edgeChar}`), - `'${edgeChar}\\u${charCode.toString(16)}${edgeChar}'`, - ); - } - } - } - // Test util.inspect.styles and util.inspect.colors. { function testColorStyle(style, input) { @@ -1813,6 +1787,39 @@ test("no assertion failures 2", () => { } }); +test("escape unpaired surrogate pairs", () => { + const edgeChar = String.fromCharCode(0xd799); + const step = isDebug ? 17 : 1; + + for (let charCode = 0xd800; charCode < 0xdfff; charCode += step) { + const surrogate = String.fromCharCode(charCode); + + assert.strictEqual(util.inspect(surrogate), `'\\u${charCode.toString(16)}'`); + assert.strictEqual( + util.inspect(`${"a".repeat(200)}${surrogate}`), + `'${"a".repeat(200)}\\u${charCode.toString(16)}'`, + ); + assert.strictEqual( + util.inspect(`${surrogate}${"a".repeat(200)}`), + `'\\u${charCode.toString(16)}${"a".repeat(200)}'`, + ); + if (charCode < 0xdc00) { + const highSurrogate = surrogate; + const lowSurrogate = String.fromCharCode(charCode + 1024); + assert(!util.inspect(`${edgeChar}${highSurrogate}${lowSurrogate}${edgeChar}`).includes("\\u")); + assert.strictEqual( + (util.inspect(`${highSurrogate}${highSurrogate}${lowSurrogate}`).match(/\\u/g) ?? []).length, + 1, + ); + } else { + assert.strictEqual( + util.inspect(`${edgeChar}${surrogate}${edgeChar}`), + `'${edgeChar}\\u${charCode.toString(16)}${edgeChar}'`, + ); + } + } +}); + test("util.inspect stack overflow handling", () => { // Test that a long linked list can be inspected without throwing an error. const list = {};