Skip to content

Commit c1b9be5

Browse files
committed
util: treat format arguments equally
Two changes here which bring us closer to the console standard: - Arguments to `util.format` are no longer formatted differently depending on their order, with format strings being an exception. - Format specifier formatting is now only triggered if the string actually contains a format string. Under the hood, we now use a single shared function to format the given arguments which will make the code easier to read and modify. PR-URL: #23162 Fixes: #23137 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
1 parent c979fad commit c1b9be5

File tree

3 files changed

+145
-102
lines changed

3 files changed

+145
-102
lines changed

doc/api/util.md

+31-19
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,17 @@ property take precedence over `--trace-deprecation` and
183183
<!-- YAML
184184
added: v0.5.3
185185
changes:
186+
- version: REPLACEME
187+
pr-url: https://github.com/nodejs/node/pull/23162
188+
description: The `format` argument is now only taken as such if it actually
189+
contains format specifiers.
190+
- version: REPLACEME
191+
pr-url: https://github.com/nodejs/node/pull/23162
192+
description: If the `format` argument is not a format string, the output
193+
string's formatting is no longer dependent on the type of the
194+
first argument. This change removes previously present quotes
195+
from strings that were being output when the first argument
196+
was not a string.
186197
- version: REPLACEME
187198
pr-url: https://github.com/nodejs/node/pull/17907
188199
description: The `%o` specifier's `depth` option will now fall back to the
@@ -195,11 +206,9 @@ changes:
195206
* `format` {string} A `printf`-like format string.
196207

197208
The `util.format()` method returns a formatted string using the first argument
198-
as a `printf`-like format.
199-
200-
The first argument is a string containing zero or more *placeholder* tokens.
201-
Each placeholder token is replaced with the converted value from the
202-
corresponding argument. Supported placeholders are:
209+
as a `printf`-like format string which can contain zero or more format
210+
specifiers. Each specifier is replaced with the converted value from the
211+
corresponding argument. Supported specifiers are:
203212

204213
* `%s` - `String`.
205214
* `%d` - `Number` (integer or floating point value) or `BigInt`.
@@ -218,37 +227,40 @@ contains circular references.
218227
* `%%` - single percent sign (`'%'`). This does not consume an argument.
219228
* Returns: {string} The formatted string
220229

221-
If the placeholder does not have a corresponding argument, the placeholder is
222-
not replaced.
230+
If a specifier does not have a corresponding argument, it is not replaced:
223231

224232
```js
225233
util.format('%s:%s', 'foo');
226234
// Returns: 'foo:%s'
227235
```
228236

229-
If there are more arguments passed to the `util.format()` method than the number
230-
of placeholders, the extra arguments are coerced into strings then concatenated
231-
to the returned string, each delimited by a space. Excessive arguments whose
232-
`typeof` is `'object'` or `'symbol'` (except `null`) will be transformed by
233-
`util.inspect()`.
237+
Values that are not part of the format string are formatted using
238+
`util.inspect()` if their type is either `'object'`, `'symbol'`, `'function'`
239+
or `'number'` and using `String()` in all other cases.
240+
241+
If there are more arguments passed to the `util.format()` method than the
242+
number of specifiers, the extra arguments are concatenated to the returned
243+
string, separated by spaces:
234244

235245
```js
236-
util.format('%s:%s', 'foo', 'bar', 'baz'); // 'foo:bar baz'
246+
util.format('%s:%s', 'foo', 'bar', 'baz');
247+
// Returns: 'foo:bar baz'
237248
```
238249

239-
If the first argument is not a string then `util.format()` returns
240-
a string that is the concatenation of all arguments separated by spaces.
241-
Each argument is converted to a string using `util.inspect()`.
250+
If the first argument does not contain a valid format specifier, `util.format()`
251+
returns a string that is the concatenation of all arguments separated by spaces:
242252

243253
```js
244-
util.format(1, 2, 3); // '1 2 3'
254+
util.format(1, 2, 3);
255+
// Returns: '1 2 3'
245256
```
246257

247258
If only one argument is passed to `util.format()`, it is returned as it is
248-
without any formatting.
259+
without any formatting:
249260

250261
```js
251-
util.format('%% %s'); // '%% %s'
262+
util.format('%% %s');
263+
// Returns: '%% %s'
252264
```
253265

254266
Please note that `util.format()` is a synchronous method that is mainly

lib/util.js

+98-83
Original file line numberDiff line numberDiff line change
@@ -72,98 +72,113 @@ function format(...args) {
7272
return formatWithOptions(emptyOptions, ...args);
7373
}
7474

75-
function formatWithOptions(inspectOptions, f) {
76-
let i, tempStr;
77-
if (typeof f !== 'string') {
78-
if (arguments.length === 1) return '';
79-
let res = '';
80-
for (i = 1; i < arguments.length - 1; i++) {
81-
res += inspect(arguments[i], inspectOptions);
82-
res += ' ';
83-
}
84-
res += inspect(arguments[i], inspectOptions);
85-
return res;
75+
function formatValue(val, inspectOptions) {
76+
const inspectTypes = ['object', 'symbol', 'function', 'number'];
77+
78+
if (inspectTypes.includes(typeof val)) {
79+
return inspect(val, inspectOptions);
80+
} else {
81+
return String(val);
82+
}
83+
}
84+
85+
function formatWithOptions(inspectOptions, ...args) {
86+
const first = args[0];
87+
const parts = [];
88+
89+
const firstIsString = typeof first === 'string';
90+
91+
if (firstIsString && args.length === 1) {
92+
return first;
8693
}
8794

88-
if (arguments.length === 2) return f;
89-
90-
let str = '';
91-
let a = 2;
92-
let lastPos = 0;
93-
for (i = 0; i < f.length - 1; i++) {
94-
if (f.charCodeAt(i) === 37) { // '%'
95-
const nextChar = f.charCodeAt(++i);
96-
if (a !== arguments.length) {
97-
switch (nextChar) {
98-
case 115: // 's'
99-
tempStr = String(arguments[a++]);
100-
break;
101-
case 106: // 'j'
102-
tempStr = tryStringify(arguments[a++]);
103-
break;
104-
case 100: // 'd'
105-
const tempNum = arguments[a++];
106-
// eslint-disable-next-line valid-typeof
107-
if (typeof tempNum === 'bigint') {
108-
tempStr = `${tempNum}n`;
109-
} else {
110-
tempStr = `${Number(tempNum)}`;
95+
if (firstIsString && /%[sjdOoif%]/.test(first)) {
96+
let i, tempStr;
97+
let str = '';
98+
let a = 1;
99+
let lastPos = 0;
100+
101+
for (i = 0; i < first.length - 1; i++) {
102+
if (first.charCodeAt(i) === 37) { // '%'
103+
const nextChar = first.charCodeAt(++i);
104+
if (a !== args.length) {
105+
switch (nextChar) {
106+
case 115: // 's'
107+
tempStr = String(args[a++]);
108+
break;
109+
case 106: // 'j'
110+
tempStr = tryStringify(args[a++]);
111+
break;
112+
case 100: // 'd'
113+
const tempNum = args[a++];
114+
// eslint-disable-next-line valid-typeof
115+
if (typeof tempNum === 'bigint') {
116+
tempStr = `${tempNum}n`;
117+
} else {
118+
tempStr = `${Number(tempNum)}`;
119+
}
120+
break;
121+
case 79: // 'O'
122+
tempStr = inspect(args[a++], inspectOptions);
123+
break;
124+
case 111: // 'o'
125+
{
126+
const opts = Object.assign({}, inspectOptions, {
127+
showHidden: true,
128+
showProxy: true,
129+
depth: 4
130+
});
131+
tempStr = inspect(args[a++], opts);
132+
break;
111133
}
112-
break;
113-
case 79: // 'O'
114-
tempStr = inspect(arguments[a++], inspectOptions);
115-
break;
116-
case 111: // 'o'
117-
{
118-
const opts = Object.assign({}, inspectOptions, {
119-
showHidden: true,
120-
showProxy: true
121-
});
122-
tempStr = inspect(arguments[a++], opts);
123-
break;
134+
case 105: // 'i'
135+
const tempInteger = args[a++];
136+
// eslint-disable-next-line valid-typeof
137+
if (typeof tempInteger === 'bigint') {
138+
tempStr = `${tempInteger}n`;
139+
} else {
140+
tempStr = `${parseInt(tempInteger)}`;
141+
}
142+
break;
143+
case 102: // 'f'
144+
tempStr = `${parseFloat(args[a++])}`;
145+
break;
146+
case 37: // '%'
147+
str += first.slice(lastPos, i);
148+
lastPos = i + 1;
149+
continue;
150+
default: // any other character is not a correct placeholder
151+
continue;
124152
}
125-
case 105: // 'i'
126-
const tempInteger = arguments[a++];
127-
// eslint-disable-next-line valid-typeof
128-
if (typeof tempInteger === 'bigint') {
129-
tempStr = `${tempInteger}n`;
130-
} else {
131-
tempStr = `${parseInt(tempInteger)}`;
132-
}
133-
break;
134-
case 102: // 'f'
135-
tempStr = `${parseFloat(arguments[a++])}`;
136-
break;
137-
case 37: // '%'
138-
str += f.slice(lastPos, i);
139-
lastPos = i + 1;
140-
continue;
141-
default: // any other character is not a correct placeholder
142-
continue;
153+
if (lastPos !== i - 1) {
154+
str += first.slice(lastPos, i - 1);
155+
}
156+
str += tempStr;
157+
lastPos = i + 1;
158+
} else if (nextChar === 37) {
159+
str += first.slice(lastPos, i);
160+
lastPos = i + 1;
143161
}
144-
if (lastPos !== i - 1)
145-
str += f.slice(lastPos, i - 1);
146-
str += tempStr;
147-
lastPos = i + 1;
148-
} else if (nextChar === 37) {
149-
str += f.slice(lastPos, i);
150-
lastPos = i + 1;
151162
}
152163
}
153-
}
154-
if (lastPos === 0)
155-
str = f;
156-
else if (lastPos < f.length)
157-
str += f.slice(lastPos);
158-
while (a < arguments.length) {
159-
const x = arguments[a++];
160-
if ((typeof x !== 'object' && typeof x !== 'symbol') || x === null) {
161-
str += ` ${x}`;
162-
} else {
163-
str += ` ${inspect(x, inspectOptions)}`;
164+
if (lastPos === 0) {
165+
str = first;
166+
} else if (lastPos < first.length) {
167+
str += first.slice(lastPos);
168+
}
169+
170+
parts.push(str);
171+
while (a < args.length) {
172+
parts.push(formatValue(args[a], inspectOptions));
173+
a++;
174+
}
175+
} else {
176+
for (const arg of args) {
177+
parts.push(formatValue(arg, inspectOptions));
164178
}
165179
}
166-
return str;
180+
181+
return parts.join(' ');
167182
}
168183

169184
const debugs = {};

test/parallel/test-util-format.js

+16
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,10 @@ assert.strictEqual(util.format('percent: %d%, fraction: %d', 10, 0.1),
273273
'percent: 10%, fraction: 0.1');
274274
assert.strictEqual(util.format('abc%', 1), 'abc% 1');
275275

276+
// Additional arguments after format specifiers
277+
assert.strictEqual(util.format('%i', 1, 'number'), '1 number');
278+
assert.strictEqual(util.format('%i', 1, () => {}), '1 [Function]');
279+
276280
{
277281
const o = {};
278282
o.o = o;
@@ -315,3 +319,15 @@ function BadCustomError(msg) {
315319
util.inherits(BadCustomError, Error);
316320
assert.strictEqual(util.format(new BadCustomError('foo')),
317321
'[BadCustomError: foo]');
322+
323+
// The format of arguments should not depend on type of the first argument
324+
assert.strictEqual(util.format('1', '1'), '1 1');
325+
assert.strictEqual(util.format(1, '1'), '1 1');
326+
assert.strictEqual(util.format('1', 1), '1 1');
327+
assert.strictEqual(util.format(1, 1), '1 1');
328+
assert.strictEqual(util.format('1', () => {}), '1 [Function]');
329+
assert.strictEqual(util.format(1, () => {}), '1 [Function]');
330+
assert.strictEqual(util.format('1', "'"), "1 '");
331+
assert.strictEqual(util.format(1, "'"), "1 '");
332+
assert.strictEqual(util.format('1', 'number'), '1 number');
333+
assert.strictEqual(util.format(1, 'number'), '1 number');

0 commit comments

Comments
 (0)