diff --git a/src/node/internal/internal_inspect.ts b/src/node/internal/internal_inspect.ts index 1f8df638410..42a1b4fbe06 100644 --- a/src/node/internal/internal_inspect.ts +++ b/src/node/internal/internal_inspect.ts @@ -1125,9 +1125,10 @@ function formatValue( // Memorize the context for custom inspection on proxies. const context = value; + let proxies = 0; // Always check for proxies to prevent side effects and to prevent triggering // any proxy handlers. - const proxy = internal.getProxyDetails(value); + let proxy = internal.getProxyDetails(value); if (proxy !== undefined) { if (proxy === null || proxy.target === null) { return ctx.stylize('', 'special'); @@ -1135,7 +1136,18 @@ function formatValue( if (ctx.showProxy) { return formatProxy(ctx, proxy, recurseTimes); } - value = proxy.target; + do { + if (proxy === null || proxy.target === null) { + let formatted = ctx.stylize('', 'special'); + for (let i = 0; i < proxies; i++) { + formatted = `${ctx.stylize('Proxy(', 'special')}${formatted}${ctx.stylize(')', 'special')}`; + } + return formatted; + } + value = proxy.target; + proxy = internal.getProxyDetails(value); + proxies += 1; + } while (proxy !== undefined); } // Provide a hook for user-specified inspect functions. @@ -1169,8 +1181,7 @@ function formatValue( // This makes sure the recurseTimes are reported as before while using // a counter internally. const depth = ctx.depth === null ? null : ctx.depth - recurseTimes; - const isCrossContext = - proxy !== undefined || !(context instanceof Object); + const isCrossContext = proxies !== 0 || !(context instanceof Object); const ret = Function.prototype.call.call( maybeCustom, context, @@ -1206,7 +1217,15 @@ function formatValue( return ctx.stylize(`[Circular *${index}]`, 'special'); } - return formatRaw(ctx, value, recurseTimes, typedArray); + let formatted = formatRaw(ctx, value, recurseTimes, typedArray); + + if (proxies !== 0) { + for (let i = 0; i < proxies; i++) { + formatted = `${ctx.stylize('Proxy(', 'special')}${formatted}${ctx.stylize(')', 'special')}`; + } + } + + return formatted; } function formatRaw( @@ -2653,10 +2672,10 @@ function hasBuiltInToString(value: object): boolean { // Prevent triggering proxy traps. const proxyTarget = internal.getProxyDetails(value); if (proxyTarget !== undefined) { - if (proxyTarget === null) { + if (proxyTarget === null || proxyTarget.target === null) { return true; } - value = proxyTarget.target as object; + return hasBuiltInToString(proxyTarget.target as object); } // Count objects that have no `toString` function as built-in. diff --git a/src/node/repl.ts b/src/node/repl.ts index 5a684f1d620..d1fc7a488cf 100644 --- a/src/node/repl.ts +++ b/src/node/repl.ts @@ -35,6 +35,22 @@ export function writer(): ReplType.REPLWriter & { options: InspectOptions } { throw new ERR_METHOD_NOT_IMPLEMENTED('writer'); } +const writerOptions: InspectOptions = { + showHidden: false, + depth: 2, + colors: true, + customInspect: true, + showProxy: true, + maxArrayLength: 100, + maxStringLength: 10000, + breakLength: 80, + compact: 3, + sorted: false, + getters: false, + numericSeparator: false, +}; +Object.assign(writer, { options: writerOptions }); + export function start(): ReplType.REPLServer { throw new ERR_METHOD_NOT_IMPLEMENTED('start'); } diff --git a/src/workerd/api/node/tests/assert-test.js b/src/workerd/api/node/tests/assert-test.js index ac16ca7d6ac..73bfe8e284c 100644 --- a/src/workerd/api/node/tests/assert-test.js +++ b/src/workerd/api/node/tests/assert-test.js @@ -46,6 +46,7 @@ import { } from 'node:assert'; import { mock } from 'node:test'; +import util from 'node:util'; import { default as assert } from 'node:assert'; @@ -242,6 +243,31 @@ export const test_deep_equal_errors = { }, }; +export const test_check_proxies = { + test() { + const arrProxy = new Proxy([1, 2], {}); + deepStrictEqual(arrProxy, [1, 2]); + const defaultMsgStartFull = `${start}\n${actExp}`; + const tmp = util.inspect.defaultOptions; + util.inspect.defaultOptions = { showProxy: true }; + throws(() => deepStrictEqual(arrProxy, [1, 2, 3]), { + message: + `${defaultMsgStartFull}\n\n` + '+ Proxy([ 1, 2 ])\n' + '- [ 1, 2, 3 ]', + }); + util.inspect.defaultOptions = tmp; + + const invalidTrap = new Proxy([1, 2, 3], { + ownKeys() { + return []; + }, + }); + throws(() => deepStrictEqual(invalidTrap, [1, 2, 3]), { + name: 'TypeError', + message: "'ownKeys' on proxy: trap result did not include 'length'", + }); + }, +}; + export const test_partial_deep_strict_equal = { test(ctrl, env, ctx) { // Test basic object partial equality diff --git a/src/workerd/api/node/tests/util-nodejs-test.js b/src/workerd/api/node/tests/util-nodejs-test.js index 0bc7056ef33..ee2712b8bb7 100644 --- a/src/workerd/api/node/tests/util-nodejs-test.js +++ b/src/workerd/api/node/tests/util-nodejs-test.js @@ -3713,6 +3713,17 @@ export const utilInspectProxy = { // Make sure inspecting object does not trigger any proxy traps. util.format('%s', proxyObj); + // %i%f%d use Symbol.toPrimitive to convert the value to a string. + // %j uses JSON.stringify, accessing the value's toJSON and toString method. + util.format('%s%o%O%c', proxyObj, proxyObj, proxyObj, proxyObj); + const nestedFormatProxy = new Proxy(new Proxy({}, handler), {}); + util.format( + '%s%o%O%c', + nestedFormatProxy, + nestedFormatProxy, + nestedFormatProxy, + nestedFormatProxy + ); const r = Proxy.revocable({}, {}); r.revoke(); @@ -3760,7 +3771,7 @@ export const utilInspectProxy = { const proxy4 = new Proxy(proxy1, proxy2); const proxy5 = new Proxy(proxy3, proxy4); const proxy6 = new Proxy(proxy5, proxy5); - const expected0 = '{}'; + const expected0 = 'Proxy({})'; const expected1 = 'Proxy [ {}, {} ]'; const expected2 = 'Proxy [ Proxy [ {}, {} ], {} ]'; const expected3 = @@ -3783,6 +3794,10 @@ export const utilInspectProxy = { ' Proxy [ Proxy [Array], Proxy [Array] ]\n' + ' ]\n' + ']'; + const expected2NoShowProxy = 'Proxy(Proxy({}))'; + const expected3NoShowProxy = 'Proxy(Proxy(Proxy({})))'; + const expected4NoShowProxy = 'Proxy(Proxy(Proxy(Proxy({}))))'; + const expected5NoShowProxy = 'Proxy(Proxy(Proxy(Proxy(Proxy({})))))'; assert.strictEqual( util.inspect(proxy1, { showProxy: 1, depth: null }), expected1 @@ -3793,17 +3808,17 @@ export const utilInspectProxy = { assert.strictEqual(util.inspect(proxy5, opts), expected5); assert.strictEqual(util.inspect(proxy6, opts), expected6); assert.strictEqual(util.inspect(proxy1), expected0); - assert.strictEqual(util.inspect(proxy2), expected0); - assert.strictEqual(util.inspect(proxy3), expected0); - assert.strictEqual(util.inspect(proxy4), expected0); - assert.strictEqual(util.inspect(proxy5), expected0); - assert.strictEqual(util.inspect(proxy6), expected0); + assert.strictEqual(util.inspect(proxy2), expected2NoShowProxy); + assert.strictEqual(util.inspect(proxy3), expected3NoShowProxy); + assert.strictEqual(util.inspect(proxy4), expected2NoShowProxy); + assert.strictEqual(util.inspect(proxy5), expected4NoShowProxy); + assert.strictEqual(util.inspect(proxy6), expected5NoShowProxy); // Just for fun, let's create a Proxy using Arrays. const proxy7 = new Proxy([], []); const expected7 = 'Proxy [ [], [] ]'; assert.strictEqual(util.inspect(proxy7, opts), expected7); - assert.strictEqual(util.inspect(proxy7), '[]'); + assert.strictEqual(util.inspect(proxy7), 'Proxy([])'); // Now we're just getting silly, right? const proxy8 = new Proxy(Date, []); @@ -3812,8 +3827,8 @@ export const utilInspectProxy = { const expected9 = 'Proxy [ [Function: Date], [Function: String] ]'; assert.strictEqual(util.inspect(proxy8, opts), expected8); assert.strictEqual(util.inspect(proxy9, opts), expected9); - assert.strictEqual(util.inspect(proxy8), '[Function: Date]'); - assert.strictEqual(util.inspect(proxy9), '[Function: Date]'); + assert.strictEqual(util.inspect(proxy8), 'Proxy([Function: Date])'); + assert.strictEqual(util.inspect(proxy9), 'Proxy([Function: Date])'); const proxy10 = new Proxy(() => {}, {}); const proxy11 = new Proxy(() => {}, { @@ -3824,10 +3839,50 @@ export const utilInspectProxy = { return proxy11; }, }); - const expected10 = '[Function (anonymous)]'; - const expected11 = '[Function (anonymous)]'; + const expected10 = 'Proxy([Function (anonymous)])'; + const expected11 = 'Proxy([Function (anonymous)])'; assert.strictEqual(util.inspect(proxy10), expected10); assert.strictEqual(util.inspect(proxy11), expected11); + + const proxy12 = new Proxy([1, 2, 3], proxy5); + assert.strictEqual( + util.inspect(proxy12, { colors: true, breakLength: 1 }), + '\x1B[36mProxy(\x1B[39m' + + '[\n \x1B[33m1\x1B[39m,\n \x1B[33m2\x1B[39m,\n \x1B[33m3\x1B[39m\n]\x1B[36m' + + ')\x1B[39m' + ); + assert.strictEqual(util.format('%s', proxy12), 'Proxy([ 1, 2, 3 ])'); + + { + // Nested proxies should not trigger any proxy handlers. + const nestedProxy = new Proxy(new Proxy(new Proxy({}, handler), {}), {}); + + assert.strictEqual( + util.inspect(nestedProxy, { showProxy: true }), + 'Proxy [ Proxy [ Proxy [ {}, [Object] ], {} ], {} ]' + ); + assert.strictEqual( + util.inspect(nestedProxy, { showProxy: false }), + expected3NoShowProxy + ); + } + + { + // Nested revoked proxies should work as expected as well as custom + // inspection functions. + const revocable = Proxy.revocable({}, handler); + revocable.revoke(); + const nestedProxy = new Proxy(revocable.proxy, {}); + + assert.strictEqual( + util.inspect(nestedProxy, { showProxy: true }), + 'Proxy [ , {} ]' + ); + assert.strictEqual( + util.inspect(nestedProxy, { showProxy: false }), + 'Proxy()' + ); + } }, };