Skip to content

Commit b8acf10

Browse files
graememorganError Prone Team
authored and
Error Prone Team
committed
Generalise FloggerArgumentToString to catch unnecessary toStringing on the way into lenient format methods.
Flume: unknown commit The check needs a rename now. Ideas on a postcard welcome, and I'll do it in a fast follow (or first). PiperOrigin-RevId: 758121777
1 parent b1f72fb commit b8acf10

File tree

2 files changed

+74
-27
lines changed

2 files changed

+74
-27
lines changed

core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerArgumentToString.java

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static com.google.common.collect.Iterables.getOnlyElement;
2222
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
2323
import static com.google.errorprone.bugpatterns.flogger.FloggerHelpers.inferFormatSpecifier;
24+
import static com.google.errorprone.bugpatterns.formatstring.LenientFormatStringUtils.getLenientFormatStringPosition;
2425
import static com.google.errorprone.matchers.Description.NO_MATCH;
2526
import static com.google.errorprone.matchers.Matchers.allOf;
2627
import static com.google.errorprone.matchers.Matchers.anyOf;
@@ -29,7 +30,6 @@
2930
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
3031
import static com.google.errorprone.util.ASTHelpers.getReceiver;
3132
import static com.google.errorprone.util.ASTHelpers.getType;
32-
import static java.util.Arrays.stream;
3333

3434
import com.google.common.base.Ascii;
3535
import com.google.common.collect.ImmutableMap;
@@ -46,6 +46,7 @@
4646
import com.sun.source.tree.MethodInvocationTree;
4747
import com.sun.tools.javac.code.Type;
4848
import com.sun.tools.javac.code.Type.ArrayType;
49+
import java.util.EnumSet;
4950
import java.util.List;
5051
import java.util.regex.Pattern;
5152
import java.util.stream.Stream;
@@ -281,34 +282,58 @@ private static ExpressionTree getOnlyArgument(MethodInvocationTree invocation) {
281282
private static final Matcher<ExpressionTree> LOG_MATCHER =
282283
instanceMethod().onDescendantOf("com.google.common.flogger.LoggingApi").named("log");
283284

284-
static final Matcher<ExpressionTree> UNWRAPPABLE =
285-
anyOf(stream(Unwrapper.values()).map(u -> u.matcher).collect(toImmutableList()));
286-
287285
@Override
288286
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
289-
if (!LOG_MATCHER.matches(tree, state)) {
290-
return NO_MATCH;
291-
}
292287
List<? extends ExpressionTree> arguments = tree.getArguments();
293-
if (arguments.isEmpty()) {
294-
return NO_MATCH;
295-
}
296-
String formatString = ASTHelpers.constValue(arguments.get(0), String.class);
297-
if (formatString == null) {
298-
return NO_MATCH;
299-
}
300-
arguments = arguments.subList(1, arguments.size());
301-
if (arguments.stream().noneMatch(argument -> UNWRAPPABLE.matches(argument, state))) {
302-
return NO_MATCH;
288+
if (LOG_MATCHER.matches(tree, state)) {
289+
if (arguments.isEmpty()) {
290+
return NO_MATCH;
291+
}
292+
String formatString = ASTHelpers.constValue(arguments.get(0), String.class);
293+
if (formatString == null) {
294+
return NO_MATCH;
295+
}
296+
arguments = arguments.subList(1, arguments.size());
297+
return unwrapArguments(formatString, tree, arguments, EnumSet.allOf(Unwrapper.class), state);
298+
} else {
299+
var lenientFormatPosition = getLenientFormatStringPosition(tree, state);
300+
EnumSet<Unwrapper> unwrappers =
301+
EnumSet.of(Unwrapper.TO_STRING, Unwrapper.STRING_VALUE_OF, Unwrapper.STATIC_TO_STRING);
302+
if (lenientFormatPosition != -1) {
303+
for (int i = lenientFormatPosition + 1; i < arguments.size(); ++i) {
304+
var argument = arguments.get(i);
305+
var unwrapper =
306+
unwrappers.stream()
307+
.filter(u -> u.matcher.matches(argument, state))
308+
.findFirst()
309+
.orElse(null);
310+
if (unwrapper == null) {
311+
continue;
312+
}
313+
Parameter unwrapped = unwrapper.unwrap((MethodInvocationTree) argument, 's');
314+
state.reportMatch(
315+
buildDescription(argument)
316+
.setMessage(
317+
"Avoid eagerly stringifying arguments to lenient format methods. The method"
318+
+ " will stringify if needed, and the evaluation may be lazy.")
319+
.addFix(SuggestedFix.replace(argument, unwrapped.source.get(state)))
320+
.build());
321+
}
322+
}
303323
}
304-
return unwrapArguments(formatString, tree, arguments, state);
324+
return NO_MATCH;
305325
}
306326

307-
Description unwrapArguments(
327+
private Description unwrapArguments(
308328
String formatString,
309329
MethodInvocationTree tree,
310330
List<? extends ExpressionTree> arguments,
331+
EnumSet<Unwrapper> unwrappers,
311332
VisitorState state) {
333+
if (arguments.stream()
334+
.noneMatch(arg -> unwrappers.stream().anyMatch(u -> u.matcher.matches(arg, state)))) {
335+
return NO_MATCH;
336+
}
312337
SuggestedFix.Builder fix = SuggestedFix.builder();
313338
int start = 0;
314339
java.util.regex.Matcher matcher = PRINTF_TERM_CAPTURE_PATTERN.matcher(formatString);
@@ -332,7 +357,7 @@ Description unwrapArguments(
332357
if (term.length() == 2) {
333358
ExpressionTree argument = arguments.get(idx);
334359
char placeholder = term.charAt(1);
335-
Parameter unwrapped = unwrap(argument, placeholder, state);
360+
Parameter unwrapped = unwrap(argument, placeholder, unwrappers, state);
336361
if (unwrapped != null) {
337362
fix.replace(argument, unwrapped.source.get(state));
338363
placeholder = firstNonNull(unwrapped.placeholder, 's');
@@ -357,13 +382,15 @@ Description unwrapArguments(
357382
}
358383

359384
private static @Nullable Parameter unwrap(
360-
ExpressionTree argument, char placeholder, VisitorState state) {
361-
for (Unwrapper unwrapper : Unwrapper.values()) {
362-
if (unwrapper.matcher.matches(argument, state)) {
363-
return unwrapper.unwrap((MethodInvocationTree) argument, placeholder);
364-
}
365-
}
366-
return null;
385+
ExpressionTree argument,
386+
char placeholder,
387+
EnumSet<Unwrapper> unwrappers,
388+
VisitorState state) {
389+
return unwrappers.stream()
390+
.filter(unwrapper -> unwrapper.matcher.matches(argument, state))
391+
.map(unwrapper -> unwrapper.unwrap((MethodInvocationTree) argument, placeholder))
392+
.findFirst()
393+
.orElse(null);
367394
}
368395

369396
private static boolean hasSingleVarargsCompatibleArgument(

core/src/test/java/com/google/errorprone/bugpatterns/flogger/FloggerArgumentToStringTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,24 @@ public void f() {
142142
""")
143143
.doTest();
144144
}
145+
146+
@Test
147+
public void lenientFormat_explicitToStringNotNeeded() {
148+
testHelper
149+
.addSourceLines(
150+
"Test.java",
151+
"""
152+
import static com.google.common.base.Preconditions.checkArgument;
153+
154+
class Test {
155+
void test(Object x, long l) {
156+
// BUG: Diagnostic contains:
157+
checkArgument(1 == 1, "%s", x.toString());
158+
// BUG: Diagnostic contains:
159+
checkArgument(1 == 1, "%s", Long.toString(l));
160+
}
161+
}
162+
""")
163+
.doTest();
164+
}
145165
}

0 commit comments

Comments
 (0)