mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
`formatConsoleArguments` in
`packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js`
is used by the DevTools backend (via `hook.js`) to inline `console.*`
printf-style substitutions after stripping React's appended component
stack.
For `%s`/`%d`/`%i`/`%f` it consumes the next argument with
`args.splice(argumentsPointer, 1)` and formats the result.
When a format string has more specifiers than arguments, `splice`
returns an
empty array, so `arg` is `undefined` and the specifier is rendered as
text:
`%s` becomes `"undefined"`, and `%d`/`%i`/`%f` become `"NaN"`.
```js
formatConsoleArguments('%s %s', 'the');
// before: ['the undefined']
// after: ['the %s']
```
Browsers and Node's `util.format` leave an unmatched specifier as a
literal
(`console.log('%s %s', 'a')` prints `a %s`; `console.log('%d')` prints
`%d`).
So a message like `console.warn('value: %s')` was shown in DevTools as
`value: undefined` instead of `value: %s`.
This guards each of the `%d`/`%i`, `%f`, and `%s` cases on argument
availability (`argumentsPointer >= args.length`): when nothing is left
to
consume it keeps the specifier text and does not splice, mirroring the
existing trailing-`%` handling added in #36852. An explicitly passed
`undefined`/`null` argument is unchanged and still renders as
`undefined`/`null`, since a value is present at that position (the
`formats nullish values` test still passes).
## How did you test this change?
Added a regression test to the existing `formatConsoleArguments`
describe
block in `packages/react-devtools-shared/src/__tests__/utils-test.js`:
```js
it('keeps specifiers literal when no argument is supplied', () => {
expect(formatConsoleArguments('%s %s', 'the')).toEqual(['the %s']);
expect(formatConsoleArguments('%s %d', 'value')).toEqual(['value %d']);
expect(formatConsoleArguments('%s %i', 'value')).toEqual(['value %i']);
expect(formatConsoleArguments('%s %f', 'value')).toEqual(['value %f']);
});
```
Each assertion fails on `main` (it produces `['the undefined']` and
`['value NaN']`) and passes with the fix.
Commands run locally:
```
yarn test --build --project devtools packages/react-devtools-shared/src/__tests__/utils-test.js
# 51 passed, 51 total
yarn lint packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js \
packages/react-devtools-shared/src/__tests__/utils-test.js
# Lint passed.
yarn flow dom-node
# No errors!
yarn prettier-check packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js \
packages/react-devtools-shared/src/__tests__/utils-test.js
# clean
```
Cross-checked the expected output against Node `util.format`:
`util.format('%s %s', 'the')` -> `the %s`; `util.format('%d')` -> `%d`.
97 lines
2.7 KiB
JavaScript
97 lines
2.7 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
// Do not add / import anything to this file.
|
|
// This function could be used from multiple places, including hook.
|
|
|
|
// Skips CSS and object arguments, inlines other in the first argument as a template string
|
|
export default function formatConsoleArguments(
|
|
maybeMessage: any,
|
|
...inputArgs: $ReadOnlyArray<any>
|
|
): $ReadOnlyArray<any> {
|
|
if (inputArgs.length === 0 || typeof maybeMessage !== 'string') {
|
|
return [maybeMessage, ...inputArgs];
|
|
}
|
|
|
|
const args = inputArgs.slice();
|
|
|
|
let template = '';
|
|
let argumentsPointer = 0;
|
|
for (let i = 0; i < maybeMessage.length; ++i) {
|
|
const currentChar = maybeMessage[i];
|
|
if (currentChar !== '%') {
|
|
template += currentChar;
|
|
continue;
|
|
}
|
|
|
|
const nextChar = maybeMessage[i + 1];
|
|
++i;
|
|
|
|
// Only keep CSS and objects, inline other arguments
|
|
switch (nextChar) {
|
|
case 'c':
|
|
case 'O':
|
|
case 'o': {
|
|
++argumentsPointer;
|
|
template += `%${nextChar}`;
|
|
|
|
break;
|
|
}
|
|
case 'd':
|
|
case 'i': {
|
|
if (argumentsPointer >= args.length) {
|
|
// No argument left for this specifier. Keep it as a literal, like
|
|
// the browser console does, rather than emitting 'NaN'.
|
|
template += `%${nextChar}`;
|
|
break;
|
|
}
|
|
const [arg] = args.splice(argumentsPointer, 1);
|
|
template += parseInt(arg, 10).toString();
|
|
|
|
break;
|
|
}
|
|
case 'f': {
|
|
if (argumentsPointer >= args.length) {
|
|
// No argument left for this specifier. Keep it as a literal, like
|
|
// the browser console does, rather than emitting 'NaN'.
|
|
template += `%${nextChar}`;
|
|
break;
|
|
}
|
|
const [arg] = args.splice(argumentsPointer, 1);
|
|
template += parseFloat(arg).toString();
|
|
|
|
break;
|
|
}
|
|
case 's': {
|
|
if (argumentsPointer >= args.length) {
|
|
// No argument left for this specifier. Keep it as a literal, like
|
|
// the browser console does, rather than emitting 'undefined'.
|
|
template += `%${nextChar}`;
|
|
break;
|
|
}
|
|
const [arg] = args.splice(argumentsPointer, 1);
|
|
template += String(arg);
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
if (nextChar === undefined) {
|
|
// A trailing '%' with no following character. Keep it as a literal
|
|
// '%' rather than emitting the string 'undefined'.
|
|
template += '%';
|
|
} else {
|
|
template += `%${nextChar}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [template, ...args];
|
|
}
|