1818
1919import static com .google .common .collect .Iterables .getLast ;
2020import static com .google .common .collect .MoreCollectors .toOptional ;
21- import static com .google .common .collect .Streams .stream ;
2221import static com .google .errorprone .BugPattern .SeverityLevel .WARNING ;
2322import static com .google .errorprone .BugPattern .StandardTags .FRAGILE_CODE ;
2423import static com .google .errorprone .matchers .Description .NO_MATCH ;
25- import static com .google .errorprone .matchers .method .MethodMatchers .instanceMethod ;
26- import static com .google .errorprone .matchers .method .MethodMatchers .staticMethod ;
27- import static com .google .errorprone .util .ASTHelpers .getReceiver ;
2824import static com .google .errorprone .util .ASTHelpers .getSymbol ;
2925import static com .google .errorprone .util .ASTHelpers .hasAnnotation ;
3026import static com .google .errorprone .util .AnnotationNames .FORMAT_METHOD_ANNOTATION ;
31- import static java .util .stream .Stream .empty ;
27+ import static com .google .errorprone .util .AnnotationNames .FORMAT_STRING_ANNOTATION ;
28+ import static com .google .errorprone .util .AnnotationNames .LENIENT_FORMAT_STRING_ANNOTATION ;
3229
30+ import com .google .common .collect .ImmutableList ;
3331import com .google .errorprone .BugPattern ;
3432import com .google .errorprone .VisitorState ;
3533import com .google .errorprone .bugpatterns .BugChecker ;
3634import com .google .errorprone .bugpatterns .BugChecker .MethodInvocationTreeMatcher ;
35+ import com .google .errorprone .fixes .SuggestedFix ;
36+ import com .google .errorprone .fixes .SuggestedFixes ;
3737import com .google .errorprone .matchers .Description ;
38- import com .google .errorprone .matchers .Matcher ;
3938import com .sun .source .tree .ExpressionTree ;
4039import com .sun .source .tree .MethodInvocationTree ;
4140import com .sun .source .tree .MethodTree ;
41+ import com .sun .source .tree .Tree ;
4242import com .sun .source .tree .VariableTree ;
4343import com .sun .tools .javac .code .Symbol ;
4444import com .sun .tools .javac .code .Symbol .VarSymbol ;
4545import java .util .List ;
46- import java .util .stream .Stream ;
4746import org .jspecify .annotations .Nullable ;
4847
4948/** A BugPattern; see the summary. */
@@ -60,68 +59,116 @@ public final class AnnotateFormatMethod extends BugChecker implements MethodInvo
6059 " (The parameters of this method would need to be reordered to make the format string and "
6160 + "arguments the final parameters before the @FormatMethod annotation can be used.)" ;
6261
63- private static final Matcher <ExpressionTree > STRING_FORMAT =
64- staticMethod ().onClass ("java.lang.String" ).named ("format" );
65- private static final Matcher <ExpressionTree > FORMATTED =
66- instanceMethod ().onExactClass ("java.lang.String" ).named ("formatted" );
67-
6862 @ Override
6963 public Description matchMethodInvocation (MethodInvocationTree tree , VisitorState state ) {
70- VarSymbol formatString ;
71- VarSymbol formatArgs ;
72- if (STRING_FORMAT .matches (tree , state )) {
73- if (tree .getArguments ().size () != 2 ) {
74- return 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 NO_MATCH ;
64+ FormatMethodArguments args = getFormatMethodArguments (tree , state );
65+ if (args == null ) {
66+ return NO_MATCH ;
67+ }
68+ if (args .arguments ().size () < 2 ) {
69+ return NO_MATCH ;
70+ }
71+ VarSymbol formatString = asSymbol (args .arguments ().get (0 ));
72+ if (formatString == null ) {
73+ return NO_MATCH ;
74+ }
75+ for (Tree enclosing : state .getPath ()) {
76+ if (enclosing instanceof MethodTree methodTree ) {
77+ Description description = matchEnclosingMethod (state , methodTree , formatString , args );
78+ if (description != NO_MATCH ) {
79+ return description ;
80+ }
8181 }
82- formatString = asSymbol (getReceiver (tree ));
83- formatArgs = asSymbol (tree .getArguments ().get (0 ));
84- } else {
82+ }
83+ return NO_MATCH ;
84+ }
85+
86+ private static @ Nullable FormatMethodArguments getFormatMethodArguments (
87+ MethodInvocationTree tree , VisitorState state ) {
88+ ImmutableList <ExpressionTree > args = FormatStringUtils .formatMethodArguments (tree , state );
89+ if (!args .isEmpty ()) {
90+ return new FormatMethodArguments (false , args );
91+ }
92+ int index = LenientFormatStringUtils .getLenientFormatStringPosition (tree , state );
93+ if (index != -1 ) {
94+ return new FormatMethodArguments (
95+ true ,
96+ ImmutableList .copyOf (tree .getArguments ().subList (index , tree .getArguments ().size ())));
97+ }
98+ return null ;
99+ }
100+
101+ private Description matchEnclosingMethod (
102+ VisitorState state , Tree node , VarSymbol formatString , FormatMethodArguments args ) {
103+ if (!(node instanceof MethodTree methodTree )) {
85104 return NO_MATCH ;
86105 }
87- if (formatString == null || formatArgs == null ) {
106+ if (hasAnnotation ( methodTree , FORMAT_METHOD_ANNOTATION , state ) ) {
88107 return NO_MATCH ;
89108 }
109+ List <? extends VariableTree > enclosingParameters = methodTree .getParameters ();
110+ VariableTree formatParameter = findParameterWithSymbol (enclosingParameters , formatString );
111+ if (formatParameter == null ) {
112+ return NO_MATCH ;
113+ }
114+ if (hasAnnotation (formatParameter , FORMAT_STRING_ANNOTATION , state )
115+ || hasAnnotation (formatParameter , LENIENT_FORMAT_STRING_ANNOTATION , state )) {
116+ return NO_MATCH ;
117+ }
118+ if (args .lenient ()) {
119+ return handleLenient (state , args .arguments (), methodTree , formatParameter );
120+ }
121+ if (!getSymbol (methodTree ).isVarArgs ()) {
122+ return NO_MATCH ;
123+ }
124+ VarSymbol formatArgs = asSymbol (args .arguments ().get (1 ));
125+ if (formatArgs == null ) {
126+ return NO_MATCH ;
127+ }
128+ VariableTree argumentsParameter = findParameterWithSymbol (enclosingParameters , formatArgs );
129+ if (argumentsParameter == null ) {
130+ return NO_MATCH ;
131+ }
132+ if (!argumentsParameter .equals (getLast (enclosingParameters ))) {
133+ return NO_MATCH ;
134+ }
135+ // We can only generate a fix if the format string is the penultimate parameter.
136+ boolean fixable =
137+ formatParameter .equals (enclosingParameters .get (enclosingParameters .size () - 2 ));
138+ return buildDescription (methodTree )
139+ .setMessage (fixable ? message () : (message () + REORDER ))
140+ .build ();
141+ }
90142
91- return stream (state .getPath ())
92- .flatMap (
93- node -> {
94- if (!(node instanceof MethodTree methodTree )) {
95- return empty ();
96- }
97- if (!getSymbol (methodTree ).isVarArgs ()
98- || hasAnnotation (methodTree , FORMAT_METHOD_ANNOTATION , state )) {
99- return empty ();
100- }
101- List <? extends VariableTree > enclosingParameters = methodTree .getParameters ();
102- VariableTree formatParameter =
103- findParameterWithSymbol (enclosingParameters , formatString );
104- VariableTree argumentsParameter =
105- findParameterWithSymbol (enclosingParameters , formatArgs );
106- if (formatParameter == null || argumentsParameter == null ) {
107- return empty ();
108- }
109- if (!argumentsParameter .equals (getLast (enclosingParameters ))) {
110- return empty ();
111- }
112- // We can only generate a fix if the format string is the penultimate parameter.
113- boolean fixable =
114- formatParameter .equals (enclosingParameters .get (enclosingParameters .size () - 2 ));
115- return Stream .of (
116- buildDescription (methodTree )
117- .setMessage (fixable ? message () : (message () + REORDER ))
118- .build ());
119- })
120- .findFirst ()
121- .orElse (NO_MATCH );
143+ private Description handleLenient (
144+ VisitorState state ,
145+ List <ExpressionTree > args ,
146+ MethodTree methodTree ,
147+ VariableTree formatParameter ) {
148+ int formatParameterIndex = methodTree .getParameters ().indexOf (formatParameter );
149+ if (args .size () != methodTree .getParameters ().size () - formatParameterIndex ) {
150+ return NO_MATCH ;
151+ }
152+ if (args .size () == 1 ) {
153+ return NO_MATCH ;
154+ }
155+ // Check that all the parameters after the format string are passed through in order.
156+ for (int i = 1 ; i < args .size (); i ++) {
157+ if (!(getSymbol (args .get (i )) instanceof VarSymbol vs )
158+ || !vs .equals (getSymbol (methodTree .getParameters ().get (formatParameterIndex + i )))) {
159+ return NO_MATCH ;
160+ }
161+ }
162+ SuggestedFix .Builder fix = SuggestedFix .builder ();
163+ var lenientFormatString =
164+ SuggestedFixes .qualifyType (state , fix , LENIENT_FORMAT_STRING_ANNOTATION );
165+ fix .prefixWith (formatParameter , "@" + lenientFormatString + " " );
166+ return describeMatch (methodTree , fix .build ());
122167 }
123168
124- private static VariableTree findParameterWithSymbol (
169+ private record FormatMethodArguments (boolean lenient , ImmutableList <ExpressionTree > arguments ) {}
170+
171+ private static @ Nullable VariableTree findParameterWithSymbol (
125172 List <? extends VariableTree > parameters , Symbol symbol ) {
126173 return parameters .stream ()
127174 .filter (parameter -> symbol .equals (getSymbol (parameter )))
0 commit comments