1717
1818import lombok .EqualsAndHashCode ;
1919import lombok .Value ;
20- import org .openrewrite .ExecutionContext ;
21- import org .openrewrite .Preconditions ;
22- import org .openrewrite .Recipe ;
23- import org .openrewrite .TreeVisitor ;
20+ import org .jspecify .annotations .Nullable ;
21+ import org .openrewrite .*;
2422import org .openrewrite .java .JavaVisitor ;
2523import org .openrewrite .java .MethodMatcher ;
2624import org .openrewrite .java .search .UsesJavaVersion ;
2725import org .openrewrite .java .search .UsesMethod ;
28- import org .openrewrite .java .tree .*;
26+ import org .openrewrite .java .tree .Expression ;
27+ import org .openrewrite .java .tree .J ;
28+ import org .openrewrite .java .tree .JRightPadded ;
29+ import org .openrewrite .java .tree .Space ;
2930import org .openrewrite .marker .Markers ;
3031
3132import java .time .Duration ;
@@ -40,6 +41,13 @@ public class StringFormatted extends Recipe {
4041
4142 private static final MethodMatcher STRING_FORMAT = new MethodMatcher ("java.lang.String format(String, ..)" );
4243
44+ @ Option (displayName = "Add parentheses around the first argument" ,
45+ description = "Add parentheses around the first argument if it is not a simple expression. " +
46+ "Default true; if false no change will be made. " ,
47+ required = false )
48+ @ Nullable
49+ Boolean addParentheses ;
50+
4351 @ Override
4452 public String getDisplayName () {
4553 return "Prefer `String.formatted(Object...)`" ;
@@ -51,52 +59,56 @@ public String getDescription() {
5159 }
5260
5361 @ Override
54- public TreeVisitor <?, ExecutionContext > getVisitor () {
55- return Preconditions .check (
56- Preconditions .and (new UsesJavaVersion <>(17 ), new UsesMethod <>(STRING_FORMAT )),
57- new StringFormattedVisitor ());
62+ public Duration getEstimatedEffortPerOccurrence () {
63+ return Duration .ofMinutes (1 );
5864 }
5965
60- private static class StringFormattedVisitor extends JavaVisitor <ExecutionContext > {
61- @ Override
62- public J visitMethodInvocation (J .MethodInvocation methodInvocation , ExecutionContext ctx ) {
63- methodInvocation = (J .MethodInvocation ) super .visitMethodInvocation (methodInvocation , ctx );
64- if (!STRING_FORMAT .matches (methodInvocation ) || methodInvocation .getMethodType () == null ) {
65- return methodInvocation ;
66- }
66+ @ Override
67+ public TreeVisitor <?, ExecutionContext > getVisitor () {
68+ TreeVisitor <?, ExecutionContext > check = Preconditions .and (new UsesJavaVersion <>(17 ), new UsesMethod <>(STRING_FORMAT ));
69+ return Preconditions .check (check , new JavaVisitor <ExecutionContext >() {
70+ @ Override
71+ public J visitMethodInvocation (J .MethodInvocation methodInvocation , ExecutionContext ctx ) {
72+ methodInvocation = (J .MethodInvocation ) super .visitMethodInvocation (methodInvocation , ctx );
73+ if (!STRING_FORMAT .matches (methodInvocation ) || methodInvocation .getMethodType () == null ) {
74+ return methodInvocation ;
75+ }
6776
68- maybeRemoveImport ("java.lang.String.format" );
69- J .MethodInvocation mi = methodInvocation .withName (methodInvocation .getName ().withSimpleName ("formatted" ));
70- mi = mi .withMethodType (methodInvocation .getMethodType ().getDeclaringType ().getMethods ().stream ()
71- .filter (it -> it .getName ().equals ("formatted" ))
72- .findAny ()
73- .orElse (null ));
74- if (mi .getName ().getType () != null ) {
75- mi = mi .withName (mi .getName ().withType (mi .getMethodType ()));
76- }
77- List <Expression > arguments = methodInvocation .getArguments ();
78- mi = mi .withSelect (wrapperNotNeeded (arguments .get (0 )) ? arguments .get (0 ).withPrefix (Space .EMPTY ) :
79- new J .Parentheses <>(randomId (), Space .EMPTY , Markers .EMPTY ,
80- JRightPadded .build (arguments .get (0 ))));
81- mi = mi .withArguments (arguments .subList (1 , arguments .size ()));
82- if (mi .getArguments ().isEmpty ()) {
83- // To store spaces between the parenthesis of a method invocation argument list
84- // Ensures formatting recipes chained together with this one will still work as expected
85- mi = mi .withArguments (singletonList (new J .Empty (randomId (), Space .EMPTY , Markers .EMPTY )));
86- }
87- return maybeAutoFormat (methodInvocation , mi , ctx );
88- }
77+ // No change when change might be controversial, such as string concatenation
78+ List <Expression > arguments = methodInvocation .getArguments ();
79+ boolean wrapperNeeded = wrapperNeeded (arguments .get (0 ));
80+ if (Boolean .FALSE .equals (addParentheses ) && wrapperNeeded ) {
81+ return methodInvocation ;
82+ }
8983
90- private static boolean wrapperNotNeeded (Expression expression ) {
91- return expression instanceof J .Identifier ||
92- expression instanceof J .Literal ||
93- expression instanceof J .MethodInvocation ||
94- expression instanceof J .FieldAccess ;
95- }
96- }
84+ maybeRemoveImport ("java.lang.String.format" );
85+ J .MethodInvocation mi = methodInvocation .withName (methodInvocation .getName ().withSimpleName ("formatted" ));
86+ mi = mi .withMethodType (methodInvocation .getMethodType ().getDeclaringType ().getMethods ().stream ()
87+ .filter (it -> it .getName ().equals ("formatted" ))
88+ .findAny ()
89+ .orElse (null ));
90+ if (mi .getName ().getType () != null ) {
91+ mi = mi .withName (mi .getName ().withType (mi .getMethodType ()));
92+ }
93+ mi = mi .withSelect (wrapperNeeded ?
94+ new J .Parentheses <>(randomId (), Space .EMPTY , Markers .EMPTY ,
95+ JRightPadded .build (arguments .get (0 ))) :
96+ arguments .get (0 ).withPrefix (Space .EMPTY ));
97+ mi = mi .withArguments (arguments .subList (1 , arguments .size ()));
98+ if (mi .getArguments ().isEmpty ()) {
99+ // To store spaces between the parenthesis of a method invocation argument list
100+ // Ensures formatting recipes chained together with this one will still work as expected
101+ mi = mi .withArguments (singletonList (new J .Empty (randomId (), Space .EMPTY , Markers .EMPTY )));
102+ }
103+ return maybeAutoFormat (methodInvocation , mi , ctx );
104+ }
97105
98- @ Override
99- public Duration getEstimatedEffortPerOccurrence () {
100- return Duration .ofMinutes (1 );
106+ private boolean wrapperNeeded (Expression expression ) {
107+ return !(expression instanceof J .Identifier ||
108+ expression instanceof J .Literal ||
109+ expression instanceof J .MethodInvocation ||
110+ expression instanceof J .FieldAccess );
111+ }
112+ });
101113 }
102114}
0 commit comments