util: fix nested proxy inspection

Fixes: https://github.com/nodejs/node/issues/61061
PR-URL: https://github.com/nodejs/node/pull/61077
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: LiviaMedeiros <livia@cirno.name>
Reviewed-By: Juan José Arboleda <soyjuanarbol@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
This commit is contained in:
Ruben Bridgewater
2025-12-22 23:22:04 +01:00
committed by GitHub
parent 799feac6f5
commit 9120924de1
2 changed files with 59 additions and 16 deletions

View File

@@ -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('<Revoked Proxy>', 'special');
}
if (ctx.showProxy) {
if (proxy[0] === null) {
return ctx.stylize('<Revoked Proxy>', 'special');
}
return formatProxy(ctx, proxy, recurseTimes);
}
value = proxy;
do {
if (proxy === null) {
let formatted = ctx.stylize('<Revoked Proxy>', '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;

View File

@@ -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 [ <Revoked Proxy>, {} ]');
assert.strictEqual(util.inspect(nestedProxy, { showProxy: false }), 'Proxy(<Revoked Proxy>)');
}