Skip to content
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

util: if present, fallback to toString using the %s formatter #27621

Closed
wants to merge 2 commits into from
Closed
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
16 changes: 9 additions & 7 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,15 @@ as a `printf`-like format string which can contain zero or more format
specifiers. Each specifier is replaced with the converted value from the
corresponding argument. Supported specifiers are:

* `%s` - `String` will be used to convert all values except `BigInt` and
`Object`. `BigInt` values will be represented with an `n` and Objects are
inspected using `util.inspect()` with options
`{ depth: 0, colors: false, compact: 3 }`.
* `%d` - `Number` will be used to convert all values except `BigInt`.
* `%i` - `parseInt(value, 10)` is used for all values except `BigInt`.
* `%f` - `parseFloat(value)` is used for all values.
* `%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()`
with options `{ depth: 0, colors: false, compact: 3 }`.
* `%d` - `Number` will be used to convert all values except `BigInt` and
`Symbol`.
* `%i` - `parseInt(value, 10)` is used for all values except `BigInt` and
`Symbol`.
* `%f` - `parseFloat(value)` is used for all values expect `Symbol`.
* `%j` - JSON. Replaced with the string `'[Circular]'` if the argument contains
circular references.
* `%o` - `Object`. A string representation of an object with generic JavaScript
Expand Down
39 changes: 30 additions & 9 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ const { NativeModule } = require('internal/bootstrap/loaders');

let hexSlice;

const builtInObjects = new Set(
Object.getOwnPropertyNames(global).filter((e) => /^([A-Z][a-z]+)+$/.test(e))
);

const inspectDefaultOptions = Object.seal({
showHidden: false,
depth: 2,
Expand Down Expand Up @@ -1543,16 +1547,33 @@ function formatWithOptions(inspectOptions, ...args) {
switch (nextChar) {
case 115: // 's'
const tempArg = args[++a];
if (typeof tempArg !== 'string' &&
typeof tempArg !== 'function') {
tempStr = inspect(tempArg, {
...inspectOptions,
compact: 3,
colors: false,
depth: 0
});
if (typeof tempArg === 'number') {
tempStr = formatNumber(stylizeNoColor, tempArg);
// eslint-disable-next-line valid-typeof
} else if (typeof tempArg === 'bigint') {
tempStr = `${tempArg}n`;
} else {
tempStr = String(tempArg);
let constr;
if (typeof tempArg !== 'object' ||
tempArg === null ||
typeof tempArg.toString === 'function' &&
// A direct own property.
(hasOwnProperty(tempArg, 'toString') ||
// A direct own property on the constructor prototype in
// case the constructor is not an built-in object.
(constr = tempArg.constructor) &&
!builtInObjects.has(constr.name) &&
constr.prototype &&
hasOwnProperty(constr.prototype, 'toString'))) {
tempStr = String(tempArg);
} else {
tempStr = inspect(tempArg, {
...inspectOptions,
compact: 3,
colors: false,
depth: 0
});
}
}
break;
case 106: // 'j'
Expand Down
22 changes: 22 additions & 0 deletions test/parallel/test-util-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,30 @@ assert.strictEqual(util.format('%s', 42n), '42n');
assert.strictEqual(util.format('%s', Symbol('foo')), 'Symbol(foo)');
assert.strictEqual(util.format('%s', true), 'true');
assert.strictEqual(util.format('%s', { a: [1, 2, 3] }), '{ a: [Array] }');
assert.strictEqual(util.format('%s', { toString() { return 'Foo'; } }), 'Foo');
assert.strictEqual(util.format('%s', { toString: 5 }), '{ toString: 5 }');
assert.strictEqual(util.format('%s', () => 5), '() => 5');

// String format specifier including `toString` properties on the prototype.
{
class Foo { toString() { return 'Bar'; } }
assert.strictEqual(util.format('%s', new Foo()), 'Bar');
assert.strictEqual(
util.format('%s', Object.setPrototypeOf(new Foo(), null)),
'[Foo: null prototype] {}'
);
global.Foo = Foo;
assert.strictEqual(util.format('%s', new Foo()), 'Bar');
delete global.Foo;
class Bar { abc = true; }
assert.strictEqual(util.format('%s', new Bar()), 'Bar { abc: true }');
class Foobar extends Array { aaa = true; }
assert.strictEqual(
util.format('%s', new Foobar(5)),
'Foobar [ <5 empty items>, aaa: true ]'
);
}

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