`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`.
## Summary
`formatConsoleArguments` (used by the DevTools backend to inline console
substitutions) walks the format string and inlines `%s`/`%d`/`%i`/`%f`
arguments while leaving `%c`/`%o`/`%O` in place. For each `%` it reads
the **next** character to decide what to do.
When the format string ends with a lone `%` — e.g.
`console.log('Progress 100%', value)` — the character after `%` is
`undefined`, and the `default` branch ran:
```js
template += `%${nextChar}`; // -> "%undefined"
```
So the function emitted the literal text `%undefined`:
```js
formatConsoleArguments('Progress 100%', 'extra');
// before: ['Progress 100%undefined', 'extra']
// after: ['Progress 100%', 'extra']
```
Browsers render a trailing `%` in a console format string as a literal
percent sign, so this PR keeps it as `%` when there is no following
character.
## How did you test this change?
Added a `keeps a trailing percent sign` test to the existing
`formatConsoleArguments` suite in `utils-test.js` (covering both a bare
trailing `%` and one that follows another substitution). Since the
function is pure and import-free, I also verified the patched logic
against every existing case in that suite plus the new ones to confirm
there are no regressions.
<!--
Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.
Before submitting a pull request, please make sure the following is
done:
1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
10. If you haven't already, complete the CLA.
Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->
## Summary
<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->
When using React Devtools, calling `console.log('%s', null)` in userland
can cause it to throw an error:
```
TypeError: Cannot read properties of null (reading 'toString')
```
## How did you test this change?
Added a unit test.
<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
If you leave this empty, your PR will very likely be closed.
-->
See https://github.com/47ng/nuqs/issues/808.