FixStringFormatExpressions: don't strip args with no matched specifiers#943
Open
neil-mushell wants to merge 4 commits into
Open
Conversation
added 3 commits
July 16, 2026 13:38
…ders
When a String#format/formatted call's format string has zero real `%`
conversion specifiers, the recipe treated every trailing argument as
dead code and deleted it. That's wrong when the string contains `{}`
tokens (SLF4J-style placeholder syntax mistakenly used with
String#format instead of a logger) -- the arguments are almost
certainly meant to be substituted, just via the wrong API. Deleting
them erases the evidence needed to notice and fix the real bug.
Skip the arg-trim in that case: zero specifiers matched AND the format
string contains `{}`. The unrelated \n-to-%n replacement is unaffected.
Also fixes a related latent bug the above change exposed: `%n` and
`%%` do not consume an argument in java.util.Formatter, but the
existing specifier-counting loop counted them as if they did. Left
uncorrected, a format string mixing `%n` (e.g. from this recipe's own
newline replacement) with `{}` placeholders would be miscounted as
having a real specifier on a second recipe cycle, silently defeating
the new guard.
Broaden and simplify the previous commit's fix. Rather than special-
casing `{}`-style (SLF4J) placeholder syntax specifically, skip the
arg-trim step entirely whenever zero `%` conversion specifiers matched
-- not just when `{}` happens to be present. This recipe should only
delete arguments it has positive evidence are unused (trailing args
beyond what matched specifiers consume); zero specifiers gives no such
evidence either way, so leaving the call untouched is safer than
guessing, regardless of what the format string actually contains.
This also incidentally fixes a separate latent bug this change's test
coverage surfaced: for `"fmt".formatted(args...)`, the trim step's
`i == 0` keep-clause wrongly assumed index 0 of the argument list is
always the format string (true for String.format(fmt, args...), false
here, where index 0 is the first real argument). That forced one
argument to survive even when zero should remain. That branch is only
reachable when zero specifiers matched, which this change now always
skips -- no separate fix needed.
…o tests
- Cover %% combined with a real, argument-consuming specifier (previously
only tested in isolation from a real specifier).
- Add a near-verbatim reproduction of the motivating production bug: a
shadow-traffic mismatch reporter using String#format with SLF4J-style
{} placeholders, matching the actual call shape that was losing its
arguments before this fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's changed?
FixStringFormatExpressionsnow skips its arg-trimming step entirelywhenever zero
%conversion specifiers matched in the format string —not just when the string happens to contain
{}tokens. Previously itdeleted every trailing argument in that case, treating them as dead
code. Also fixes a related counting bug:
%nand%%don't consumean argument in
java.util.Formatter, but the existing loop countedthem as if they did.
What's your motivation?
Deleting arguments from a format string with zero real specifiers is
only safe if you're sure they're actually unused. That's not a safe
assumption when the string uses a different templating convention
entirely — most commonly SLF4J-style
{}placeholders mistakenly usedwith
String#formatinstead of a logger:The calling code already had a bug (wrong placeholder syntax for
String#format), but this recipe made it worse: it doesn't fix thereal problem, and it deletes the evidence (the arguments) that would
help a reviewer notice something's off. Real-world case that surfaced
this: a shadow-traffic mismatch reporter whose diagnostic messages all
degraded to the literal string
"expect {}, actual {}", with novalues, after this recipe ran on it.
Anything in particular you'd like reviewers to focus on?
Two things beyond the headline fix:
%n/%%counting fix — without it, a format string mixing%n(e.g. from this recipe's own newline replacement) with azero-specifier string would be miscounted as having a real
specifier on a later recipe cycle, silently defeating the main
fix. Caught by the
newlineStillReplacedWhenArgsAreNotStrippedtest.
.formatted()path: the trim step'si == 0keep-clause assumedindex 0 of the argument list is always the format string, true for
String.format(fmt, args...)but false for"fmt".formatted(args...)(where index 0 is the first realargument) — that previously forced one argument to survive even
when zero should remain. That branch is only reachable when zero
specifiers matched, which is now always skipped, so no separate
fix was needed. Covered by
doNotStripArgsWithNoSpecifiersEvenWithoutPlaceholders.Anyone you would like to review specifically?
No specific request — open to whoever's around.
Have you considered any alternatives or workarounds?
Yes, two:
{}-style tokens specifically(detect SLF4J placeholder syntax via a dedicated regex), rather than
bailing out on any zero-specifier case. Went with the broader
version instead: it's simpler (no new pattern needed), and it's
robust to other broken-templating conventions we haven't thought of
(e.g.
MessageFormat-style{0}), not just SLF4J. The tradeoff isit also stops trimming genuinely dead args when there's no
placeholder-like syntax at all (covered by
doNotStripArgsWithNoSpecifiersEvenWithoutPlaceholders) — areasonable cost for not having to guess.
prompted finding this in the first place. Fixing it here is
obviously preferable.
Any additional context
Full module suite: 2145 tests, 0 regressions from this change (1
pre-existing failure in
AllBranchesIdenticalTest.collapseIdenticalBranchesPython, a localPython-toolchain environment issue unrelated to this change).
Checklist