Skip to content

util: fix formatting of objects with built-in Symbol.toPrimitive #57832

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ corresponding argument. Supported specifiers are:

* `%s`: `String` will be used to convert all values except `BigInt`, `Object`
and `-0`. `BigInt` values will be represented with an `n` and Objects that
have no user defined `toString` function are inspected using `util.inspect()`
have neither a user defined `toString` function nor `Symbol.toPrimitive` function are inspected using `util.inspect()`
with options `{ depth: 0, colors: false, compact: 3 }`.
* `%d`: `Number` will be used to convert all values except `BigInt` and
`Symbol`.
Expand Down
35 changes: 22 additions & 13 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -2161,27 +2161,32 @@ function hasBuiltInToString(value) {
value = proxyTarget;
}

// Check if value has a custom Symbol.toPrimitive transformation.
if (typeof value[SymbolToPrimitive] === 'function') {
return false;
}
let hasOwnToString = ObjectPrototypeHasOwnProperty;
let hasOwnToPrimitive = ObjectPrototypeHasOwnProperty;

// Count objects that have no `toString` function as built-in.
// Count objects without `toString` and `Symbol.toPrimitive` function as built-in.
if (typeof value.toString !== 'function') {
return true;
}

// The object has a own `toString` property. Thus it's not not a built-in one.
if (ObjectPrototypeHasOwnProperty(value, 'toString')) {
if (typeof value[SymbolToPrimitive] !== 'function') {
return true;
} else if (ObjectPrototypeHasOwnProperty(value, SymbolToPrimitive)) {
return false;
}
hasOwnToString = returnFalse;
} else if (ObjectPrototypeHasOwnProperty(value, 'toString')) {
return false;
} else if (typeof value[SymbolToPrimitive] !== 'function') {
hasOwnToPrimitive = returnFalse;
} else if (ObjectPrototypeHasOwnProperty(value, SymbolToPrimitive)) {
return false;
}

// Find the object that has the `toString` property as own property in the
// prototype chain.
// Find the object that has the `toString` property or `Symbol.toPrimitive` property
// as own property in the prototype chain.
let pointer = value;
do {
pointer = ObjectGetPrototypeOf(pointer);
} while (!ObjectPrototypeHasOwnProperty(pointer, 'toString'));
} while (!hasOwnToString(pointer, 'toString') &&
!hasOwnToPrimitive(pointer, SymbolToPrimitive));

// Check closer if the object is a built-in.
const descriptor = ObjectGetOwnPropertyDescriptor(pointer, 'constructor');
Expand All @@ -2190,6 +2195,10 @@ function hasBuiltInToString(value) {
builtInObjects.has(descriptor.value.name);
}

function returnFalse() {
return false;
}
Comment on lines +2198 to +2200
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this faster than nulling out hasOwnToPrimitive and checking for that, avoiding the function call?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That likely depends on a) the inlining and b) if it is more common to have to check for both methods (if that's the case, any additional check is more expensive).


const firstErrorLine = (error) => StringPrototypeSplit(error.message, '\n', 1)[0];
let CIRCULAR_ERROR_MESSAGE;
function tryStringify(arg) {
Expand Down
62 changes: 62 additions & 0 deletions test/parallel/test-util-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,68 @@ assert.strictEqual(util.format('%s', -Infinity), '-Infinity');
assert.strictEqual(util.format('%s', objectWithToPrimitive + ''), 'default context');
}

// built-in toPrimitive is the same behavior as inspect
{
const date = new Date('2023-10-01T00:00:00Z');
assert.strictEqual(util.format('%s', date), util.inspect(date));

const symbol = Symbol('foo');
assert.strictEqual(util.format('%s', symbol), util.inspect(symbol));
}

// Prototype chain handling for toString
{
function hasToStringButNoToPrimitive() {}

hasToStringButNoToPrimitive.prototype.toString = function() {
return 'hasToStringButNoToPrimitive';
};

let obj = new hasToStringButNoToPrimitive();
assert.strictEqual(util.format('%s', obj.toString()), 'hasToStringButNoToPrimitive');

function inheritsFromHasToStringButNoToPrimitive() {}
Object.setPrototypeOf(inheritsFromHasToStringButNoToPrimitive.prototype,
hasToStringButNoToPrimitive.prototype);
obj = new inheritsFromHasToStringButNoToPrimitive();
assert.strictEqual(util.format('%s', obj.toString()), 'hasToStringButNoToPrimitive');
}

// Prototype chain handling for Symbol.toPrimitive
{
function hasToPrimitiveButNoToString() {}

hasToPrimitiveButNoToString.prototype[Symbol.toPrimitive] = function() {
return 'hasToPrimitiveButNoToString';
};

let obj = new hasToPrimitiveButNoToString();
assert.strictEqual(util.format('%s', obj[Symbol.toPrimitive]()), 'hasToPrimitiveButNoToString');
function inheritsFromHasToPrimitiveButNoToString() {}
Object.setPrototypeOf(inheritsFromHasToPrimitiveButNoToString.prototype,
hasToPrimitiveButNoToString.prototype);
obj = new inheritsFromHasToPrimitiveButNoToString();
assert.strictEqual(util.format('%s', obj[Symbol.toPrimitive]()), 'hasToPrimitiveButNoToString');
}

// Prototype chain handling for both toString and Symbol.toPrimitive
{
function hasBothToStringAndToPrimitive() {}
hasBothToStringAndToPrimitive.prototype.toString = function() {
return 'toString';
};
hasBothToStringAndToPrimitive.prototype[Symbol.toPrimitive] = function() {
return 'toPrimitive';
};
let obj = new hasBothToStringAndToPrimitive();
assert.strictEqual(util.format('%s', obj.toString()), 'toString');
function inheritsFromHasBothToStringAndToPrimitive() {}
Object.setPrototypeOf(inheritsFromHasBothToStringAndToPrimitive.prototype,
hasBothToStringAndToPrimitive.prototype);
obj = new inheritsFromHasBothToStringAndToPrimitive();
assert.strictEqual(util.format('%s', obj.toString()), 'toString');
}

// JSON format specifier
assert.strictEqual(util.format('%j'), '%j');
assert.strictEqual(util.format('%j', 42), '42');
Expand Down