Skip to content

Commit 58a9e80

Browse files
graememorganError Prone Team
authored andcommitted
Update a few checks (and a class of tests, with AbstractToString) to handle #formatted.
There are some ERROR-level checks under AbstractToString, but I think #formatted can't be used in the depot yet? PiperOrigin-RevId: 595389167
1 parent fd21bc9 commit 58a9e80

File tree

5 files changed

+121
-32
lines changed

5 files changed

+121
-32
lines changed

core/src/main/java/com/google/errorprone/bugpatterns/AbstractToString.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ protected abstract Optional<Fix> toStringFix(
9696
symbolHasAnnotation(FormatMethod.class);
9797

9898
private static final Matcher<ExpressionTree> STRING_FORMAT =
99-
staticMethod().onClass("java.lang.String").named("format");
99+
anyOf(
100+
staticMethod().onClass("java.lang.String").named("format"),
101+
instanceMethod().onExactClass("java.lang.String").named("formatted"));
100102

101103
private static final Matcher<ExpressionTree> VALUE_OF =
102104
staticMethod()

core/src/main/java/com/google/errorprone/bugpatterns/AnnotateFormatMethod.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import static com.google.common.collect.MoreCollectors.toOptional;
2121
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
2222
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
23+
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
2324
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
25+
import static com.google.errorprone.util.ASTHelpers.getReceiver;
2426

2527
import com.google.errorprone.BugPattern;
2628
import com.google.errorprone.VisitorState;
@@ -47,9 +49,9 @@
4749
*/
4850
@BugPattern(
4951
summary =
50-
"This method passes a pair of parameters through to String.format, but the enclosing"
51-
+ " method wasn't annotated @FormatMethod. Doing so gives compile-time rather than"
52-
+ " run-time protection against malformed format strings.",
52+
"This method uses a pair of parameters as a format string and its arguments, but the"
53+
+ " enclosing method wasn't annotated @FormatMethod. Doing so gives compile-time rather"
54+
+ " than run-time protection against malformed format strings.",
5355
tags = FRAGILE_CODE,
5456
severity = WARNING)
5557
public final class AnnotateFormatMethod extends BugChecker implements MethodInvocationTreeMatcher {
@@ -60,17 +62,28 @@ public final class AnnotateFormatMethod extends BugChecker implements MethodInvo
6062

6163
private static final Matcher<ExpressionTree> STRING_FORMAT =
6264
staticMethod().onClass("java.lang.String").named("format");
65+
private static final Matcher<ExpressionTree> FORMATTED =
66+
instanceMethod().onExactClass("java.lang.String").named("formatted");
6367

6468
@Override
6569
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
66-
if (!STRING_FORMAT.matches(tree, state)) {
70+
VarSymbol formatString;
71+
VarSymbol formatArgs;
72+
if (STRING_FORMAT.matches(tree, state)) {
73+
if (tree.getArguments().size() != 2) {
74+
return Description.NO_MATCH;
75+
}
76+
formatString = asSymbol(tree.getArguments().get(0));
77+
formatArgs = asSymbol(tree.getArguments().get(1));
78+
} else if (FORMATTED.matches(tree, state)) {
79+
if (tree.getArguments().size() != 1) {
80+
return Description.NO_MATCH;
81+
}
82+
formatString = asSymbol(getReceiver(tree));
83+
formatArgs = asSymbol(tree.getArguments().get(0));
84+
} else {
6785
return Description.NO_MATCH;
6886
}
69-
if (tree.getArguments().size() != 2) {
70-
return Description.NO_MATCH;
71-
}
72-
VarSymbol formatString = asSymbol(tree.getArguments().get(0));
73-
VarSymbol formatArgs = asSymbol(tree.getArguments().get(1));
7487
if (formatString == null || formatArgs == null) {
7588
return Description.NO_MATCH;
7689
}

core/src/main/java/com/google/errorprone/bugpatterns/StringFormatWithLiteral.java

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package com.google.errorprone.bugpatterns;
1818

19+
import static com.google.common.collect.ImmutableList.toImmutableList;
1920
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
21+
import static com.google.errorprone.matchers.Matchers.instanceMethod;
2022
import static com.google.errorprone.matchers.Matchers.staticMethod;
23+
import static com.google.errorprone.util.ASTHelpers.getReceiver;
2124

25+
import com.google.common.collect.ImmutableList;
2226
import com.google.errorprone.BugPattern;
2327
import com.google.errorprone.VisitorState;
2428
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
@@ -42,12 +46,30 @@ public final class StringFormatWithLiteral extends BugChecker
4246
private static final Matcher<ExpressionTree> STRING_FORMAT_METHOD_MATCHER =
4347
staticMethod().onClass("java.lang.String").named("format");
4448

49+
private static final Matcher<ExpressionTree> FORMATTED =
50+
instanceMethod().onExactClass("java.lang.String").named("formatted");
51+
4552
private static final Pattern SPECIFIER_ALLOW_LIST_REGEX = Pattern.compile("%(d|s|S|c|b|B)");
4653

4754
@Override
4855
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
49-
if (STRING_FORMAT_METHOD_MATCHER.matches(tree, state) && shouldRefactorStringFormat(tree)) {
50-
return describeMatch(tree, refactor(tree));
56+
if (STRING_FORMAT_METHOD_MATCHER.matches(tree, state)) {
57+
ImmutableList<ExpressionTree> arguments =
58+
tree.getArguments().stream().skip(1).collect(toImmutableList());
59+
if (shouldRefactorStringFormat(tree.getArguments().get(0), arguments)) {
60+
return describeMatch(
61+
tree,
62+
SuggestedFix.replace(
63+
tree, getFormattedUnifiedString(tree.getArguments().get(0), arguments)));
64+
}
65+
}
66+
if (FORMATTED.matches(tree, state)) {
67+
if (shouldRefactorStringFormat(getReceiver(tree), tree.getArguments())) {
68+
return describeMatch(
69+
tree,
70+
SuggestedFix.replace(
71+
tree, getFormattedUnifiedString(getReceiver(tree), tree.getArguments())));
72+
}
5173
}
5274
return Description.NO_MATCH;
5375
}
@@ -57,15 +79,15 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState
5779
* string included). Format strings (first argument) as variables or constants are excluded from
5880
* refactoring. The refactoring also has an allowlist of "non trivial" formatting specifiers. This
5981
* is done since there are some instances where the String.format() invocation is justified even
60-
* with
82+
* with a CONSTANT but non-literal format string.
6183
*/
62-
private static boolean shouldRefactorStringFormat(MethodInvocationTree tree) {
63-
if (!tree.getArguments().stream()
64-
.allMatch(argumentTree -> argumentTree instanceof LiteralTree)) {
84+
private static boolean shouldRefactorStringFormat(
85+
ExpressionTree formatString, List<? extends ExpressionTree> arguments) {
86+
if (!(formatString instanceof LiteralTree)
87+
|| !arguments.stream().allMatch(argumentTree -> argumentTree instanceof LiteralTree)) {
6588
return false;
6689
}
67-
LiteralTree formatString = (LiteralTree) tree.getArguments().get(0);
68-
return onlyContainsSpecifiersInAllowList((String) formatString.getValue());
90+
return onlyContainsSpecifiersInAllowList((String) ((LiteralTree) formatString).getValue());
6991
}
7092

7193
private static boolean onlyContainsSpecifiersInAllowList(String formatString) {
@@ -74,29 +96,18 @@ private static boolean onlyContainsSpecifiersInAllowList(String formatString) {
7496
return !noSpecifierFormatBase.contains("%");
7597
}
7698

77-
private static SuggestedFix refactor(MethodInvocationTree tree) {
78-
return SuggestedFix.replace(
79-
tree, getFormattedUnifiedString(getFormatString(tree), tree.getArguments()));
80-
}
81-
8299
/**
83100
* Formats the string originally on the String.format to be a unified string with all the literal
84101
* parameters, when available.
85102
*/
86103
private static String getFormattedUnifiedString(
87-
String formatString, List<? extends ExpressionTree> arguments) {
104+
ExpressionTree formatString, List<? extends ExpressionTree> arguments) {
88105
String unescapedFormatString =
89106
String.format(
90-
formatString,
107+
(String) ((LiteralTree) formatString).getValue(),
91108
arguments.stream()
92-
.skip(1) // skip the format string argument.
93-
.map(literallTree -> ((LiteralTree) literallTree).getValue())
109+
.map(literalTree -> ((LiteralTree) literalTree).getValue())
94110
.toArray(Object[]::new));
95111
return '"' + SourceCodeEscapers.javaCharEscaper().escape(unescapedFormatString) + '"';
96112
}
97-
98-
private static String getFormatString(MethodInvocationTree tree) {
99-
LiteralTree formatStringTree = (LiteralTree) tree.getArguments().get(0);
100-
return formatStringTree.getValue().toString();
101-
}
102113
}

core/src/test/java/com/google/errorprone/bugpatterns/AnnotateFormatMethodTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
package com.google.errorprone.bugpatterns;
1818

19+
import static org.junit.Assume.assumeTrue;
20+
1921
import com.google.errorprone.CompilationTestHelper;
22+
import com.google.errorprone.util.RuntimeVersion;
2023
import org.junit.Test;
2124
import org.junit.runner.RunWith;
2225
import org.junit.runners.JUnit4;
@@ -45,6 +48,21 @@ public void positiveCase() {
4548
.doTest();
4649
}
4750

51+
@Test
52+
public void formatted() {
53+
assumeTrue(RuntimeVersion.isAtLeast15());
54+
compilationHelper
55+
.addSourceLines(
56+
"AnnotateFormatMethodPositiveCases.java",
57+
"class AnnotateFormatMethodPositiveCases {",
58+
" // BUG: Diagnostic contains: FormatMethod",
59+
" String formatMe(String formatString, Object... args) {",
60+
" return formatString.formatted(args);",
61+
" }",
62+
"}")
63+
.doTest();
64+
}
65+
4866
@Test
4967
public void alreadyAnnotated() {
5068
compilationHelper

core/src/test/java/com/google/errorprone/bugpatterns/StringFormatWithLiteralTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616

1717
package com.google.errorprone.bugpatterns;
1818

19+
import static org.junit.Assume.assumeTrue;
20+
1921
import com.google.errorprone.BugCheckerRefactoringTestHelper;
2022
import com.google.errorprone.CompilationTestHelper;
23+
import com.google.errorprone.util.RuntimeVersion;
2124
import org.junit.Test;
2225
import org.junit.runner.RunWith;
2326
import org.junit.runners.JUnit4;
@@ -195,6 +198,27 @@ public void refactoringStringFormatWithNoArguments() {
195198
.doTest();
196199
}
197200

201+
@Test
202+
public void refactoringFormattedWithNoArguments() {
203+
assumeTrue(RuntimeVersion.isAtLeast15());
204+
refactoringHelper
205+
.addInputLines(
206+
"ExampleClass.java",
207+
"public class ExampleClass {",
208+
" String test() {",
209+
" return \"Formatting nothing\".formatted();",
210+
" }",
211+
"}")
212+
.addOutputLines(
213+
"ExampleClass.java",
214+
"public class ExampleClass {",
215+
" String test() {",
216+
" return \"Formatting nothing\";",
217+
" }",
218+
"}")
219+
.doTest();
220+
}
221+
198222
@Test
199223
public void refactoringStringFormatWithIntegerLiteral() {
200224
refactoringHelper
@@ -215,6 +239,27 @@ public void refactoringStringFormatWithIntegerLiteral() {
215239
.doTest();
216240
}
217241

242+
@Test
243+
public void refactoringFormattedWithIntegerLiteral() {
244+
assumeTrue(RuntimeVersion.isAtLeast15());
245+
refactoringHelper
246+
.addInputLines(
247+
"ExampleClass.java",
248+
"public class ExampleClass {",
249+
" String test() {",
250+
" return \"Formatting this integer: %d\".formatted(1);",
251+
" }",
252+
"}")
253+
.addOutputLines(
254+
"ExampleClass.java",
255+
"public class ExampleClass {",
256+
" String test() {",
257+
" return \"Formatting this integer: 1\";",
258+
" }",
259+
"}")
260+
.doTest();
261+
}
262+
218263
@Test
219264
public void refactoringStringFormatWithBooleanLiteral() {
220265
refactoringHelper

0 commit comments

Comments
 (0)