Skip to content

Commit 5518664

Browse files
committed
util: if present, fallback to toString using the %s formatter
This makes sure that `util.format` uses `String` to stringify an object in case the object has an own property named `toString` with type `function`. That way objects that do not have such function are still inspected using `util.inspect` and the old behavior is preserved as well. PR-URL: nodejs#27621 Refs: jestjs/jest#8443 Reviewed-By: Roman Reiss <[email protected]>
1 parent 182b48a commit 5518664

File tree

3 files changed

+55
-12
lines changed

3 files changed

+55
-12
lines changed

doc/api/util.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,9 @@ specifiers. Each specifier is replaced with the converted value from the
221221
corresponding argument. Supported specifiers are:
222222

223223
* `%s` - `String` will be used to convert all values except `BigInt`, `Object`
224-
and `-0`. `BigInt` values will be represented with an `n` and Objects are
225-
inspected using `util.inspect()` with options
226-
`{ depth: 0, colors: false, compact: 3 }`.
224+
and `-0`. `BigInt` values will be represented with an `n` and Objects that
225+
have no user defined `toString` function are inspected using `util.inspect()`
226+
with options `{ depth: 0, colors: false, compact: 3 }`.
227227
* `%d` - `Number` will be used to convert all values except `BigInt` and
228228
`Symbol`.
229229
* `%i` - `parseInt(value, 10)` is used for all values except `BigInt` and

lib/internal/util/inspect.js

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ const { NativeModule } = require('internal/bootstrap/loaders');
9393

9494
let hexSlice;
9595

96+
const builtInObjects = new Set(
97+
Object.getOwnPropertyNames(global).filter((e) => /^([A-Z][a-z]+)+$/.test(e))
98+
);
99+
96100
const inspectDefaultOptions = Object.seal({
97101
showHidden: false,
98102
depth: 2,
@@ -1541,16 +1545,33 @@ function formatWithOptions(inspectOptions, ...args) {
15411545
switch (nextChar) {
15421546
case 115: // 's'
15431547
const tempArg = args[++a];
1544-
if (typeof tempArg !== 'string' &&
1545-
typeof tempArg !== 'function') {
1546-
tempStr = inspect(tempArg, {
1547-
...inspectOptions,
1548-
compact: 3,
1549-
colors: false,
1550-
depth: 0
1551-
});
1548+
if (typeof tempArg === 'number') {
1549+
tempStr = formatNumber(stylizeNoColor, tempArg);
1550+
// eslint-disable-next-line valid-typeof
1551+
} else if (typeof tempArg === 'bigint') {
1552+
tempStr = `${tempArg}n`;
15521553
} else {
1553-
tempStr = String(tempArg);
1554+
let constr;
1555+
if (typeof tempArg !== 'object' ||
1556+
tempArg === null ||
1557+
typeof tempArg.toString === 'function' &&
1558+
// A direct own property.
1559+
(hasOwnProperty(tempArg, 'toString') ||
1560+
// A direct own property on the constructor prototype in
1561+
// case the constructor is not an built-in object.
1562+
(constr = tempArg.constructor) &&
1563+
!builtInObjects.has(constr.name) &&
1564+
constr.prototype &&
1565+
hasOwnProperty(constr.prototype, 'toString'))) {
1566+
tempStr = String(tempArg);
1567+
} else {
1568+
tempStr = inspect(tempArg, {
1569+
...inspectOptions,
1570+
compact: 3,
1571+
colors: false,
1572+
depth: 0
1573+
});
1574+
}
15541575
}
15551576
break;
15561577
case 106: // 'j'

test/parallel/test-util-format.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,30 @@ assert.strictEqual(util.format('%s', 42n), '42n');
138138
assert.strictEqual(util.format('%s', Symbol('foo')), 'Symbol(foo)');
139139
assert.strictEqual(util.format('%s', true), 'true');
140140
assert.strictEqual(util.format('%s', { a: [1, 2, 3] }), '{ a: [Array] }');
141+
assert.strictEqual(util.format('%s', { toString() { return 'Foo'; } }), 'Foo');
142+
assert.strictEqual(util.format('%s', { toString: 5 }), '{ toString: 5 }');
141143
assert.strictEqual(util.format('%s', () => 5), '() => 5');
142144

145+
// String format specifier including `toString` properties on the prototype.
146+
{
147+
class Foo { toString() { return 'Bar'; } }
148+
assert.strictEqual(util.format('%s', new Foo()), 'Bar');
149+
assert.strictEqual(
150+
util.format('%s', Object.setPrototypeOf(new Foo(), null)),
151+
'[Foo: null prototype] {}'
152+
);
153+
global.Foo = Foo;
154+
assert.strictEqual(util.format('%s', new Foo()), 'Bar');
155+
delete global.Foo;
156+
class Bar { abc = true; }
157+
assert.strictEqual(util.format('%s', new Bar()), 'Bar { abc: true }');
158+
class Foobar extends Array { aaa = true; }
159+
assert.strictEqual(
160+
util.format('%s', new Foobar(5)),
161+
'Foobar [ <5 empty items>, aaa: true ]'
162+
);
163+
}
164+
143165
// JSON format specifier
144166
assert.strictEqual(util.format('%j'), '%j');
145167
assert.strictEqual(util.format('%j', 42), '42');

0 commit comments

Comments
 (0)