diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index d5da624ed5a16e..43504de17256ec 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1118,17 +1118,29 @@ function formatValue(ctx, value, recurseTimes, typedArray) { // 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 = getProxyDetails(value, !!ctx.showProxy); + let proxy = getProxyDetails(value, !!ctx.showProxy); if (proxy !== undefined) { - if (proxy === null || proxy[0] === null) { - return ctx.stylize('', 'special'); - } if (ctx.showProxy) { + if (proxy[0] === null) { + return ctx.stylize('', 'special'); + } return formatProxy(ctx, proxy, recurseTimes); } - value = proxy; + do { + if (proxy === 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; + proxy = getProxyDetails(value, false); + proxies += 1; + } while (proxy !== undefined); } // Provide a hook for user-specified inspect functions. @@ -1144,7 +1156,7 @@ function formatValue(ctx, value, recurseTimes, typedArray) { // a counter internally. const depth = ctx.depth === null ? null : ctx.depth - recurseTimes; const isCrossContext = - proxy !== undefined || !FunctionPrototypeSymbolHasInstance(Object, context); + proxies !== 0 || !FunctionPrototypeSymbolHasInstance(Object, context); const ret = FunctionPrototypeCall( maybeCustom, context, @@ -1180,10 +1192,12 @@ function formatValue(ctx, value, recurseTimes, typedArray) { return ctx.stylize(`[Circular *${index}]`, 'special'); } - const formatted = formatRaw(ctx, value, recurseTimes, typedArray); + let formatted = formatRaw(ctx, value, recurseTimes, typedArray); - if (proxy !== undefined) { - return `${ctx.stylize('Proxy(', 'special')}${formatted}${ctx.stylize(')', 'special')}`; + if (proxies !== 0) { + for (let i = 0; i < proxies; i++) { + formatted = `${ctx.stylize('Proxy(', 'special')}${formatted}${ctx.stylize(')', 'special')}`; + } } return formatted; @@ -2691,7 +2705,7 @@ function hasBuiltInToString(value) { if (proxyTarget === null) { return true; } - value = proxyTarget; + return hasBuiltInToString(proxyTarget); } let hasOwnToString = ObjectPrototypeHasOwnProperty; diff --git a/test/parallel/test-util-inspect-proxy.js b/test/parallel/test-util-inspect-proxy.js index aa9e83c926be5f..7c9c2187edc561 100644 --- a/test/parallel/test-util-inspect-proxy.js +++ b/test/parallel/test-util-inspect-proxy.js @@ -42,7 +42,11 @@ proxyObj = new Proxy(target, handler); util.inspect(proxyObj, opts); // 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 nestedProxy = new Proxy(new Proxy({}, handler), {}); +util.format('%s%o%O%c', nestedProxy, nestedProxy, nestedProxy, nestedProxy); // getProxyDetails is an internal method, not intended for public use. // This is here to test that the internals are working correctly. @@ -135,6 +139,10 @@ const expected6 = 'Proxy [\n' + ' 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); @@ -144,11 +152,11 @@ assert.strictEqual(util.inspect(proxy4, opts), expected4); 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([], []); @@ -188,3 +196,24 @@ assert.strictEqual( ')\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()'); +}