Skip to content

Commit bd2cc9c

Browse files
authored
StringFormatted should not wrap first argument by default in Java 17 upgrade (#618)
Fixes #616
1 parent 05b8264 commit bd2cc9c

File tree

3 files changed

+92
-55
lines changed

3 files changed

+92
-55
lines changed

src/main/java/org/openrewrite/java/migrate/lang/StringFormatted.java

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@
1717

1818
import lombok.EqualsAndHashCode;
1919
import 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.*;
2422
import org.openrewrite.java.JavaVisitor;
2523
import org.openrewrite.java.MethodMatcher;
2624
import org.openrewrite.java.search.UsesJavaVersion;
2725
import 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;
2930
import org.openrewrite.marker.Markers;
3031

3132
import 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
}

src/main/resources/META-INF/rewrite/java-version-17.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ tags:
2828
recipeList:
2929
- org.openrewrite.java.migrate.Java8toJava11
3030
- org.openrewrite.java.migrate.UpgradeBuildToJava17
31-
- org.openrewrite.java.migrate.lang.StringFormatted
3231
- org.openrewrite.staticanalysis.InstanceOfPatternMatch
3332
- org.openrewrite.staticanalysis.AddSerialAnnotationToSerialVersionUID
3433
- org.openrewrite.java.migrate.RemovedRuntimeTraceMethods
3534
- org.openrewrite.java.migrate.RemovedToolProviderConstructor
3635
- org.openrewrite.java.migrate.RemovedModifierAndConstantBootstrapsConstructors
3736
- org.openrewrite.java.migrate.lang.UseTextBlocks:
3837
convertStringsWithoutNewlines: false
38+
- org.openrewrite.java.migrate.lang.StringFormatted:
39+
addParentheses: false
3940
- org.openrewrite.java.migrate.DeprecatedJavaxSecurityCert
4041
- org.openrewrite.java.migrate.DeprecatedLogRecordThreadID
4142
- org.openrewrite.java.migrate.RemovedLegacySunJSSEProviderName

src/test/java/org/openrewrite/java/migrate/lang/StringFormattedTest.java

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
class StringFormattedTest implements RewriteTest {
2828
@Override
2929
public void defaults(RecipeSpec spec) {
30-
spec.recipe(new StringFormatted());
30+
spec.recipe(new StringFormatted(null));
3131
}
3232

3333
@Test
@@ -72,12 +72,36 @@ void concatenatedText() {
7272
class A {
7373
String str = String.format("foo"
7474
+ "%s", "a");
75-
}""", """
75+
}
76+
""",
77+
"""
7678
package com.example.app;
7779
class A {
7880
String str = ("foo"
7981
+ "%s").formatted("a");
80-
}"""
82+
}
83+
"""
84+
),
85+
17
86+
)
87+
);
88+
}
89+
90+
@Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/616")
91+
@Test
92+
void concatenatedFormatStringNotConvertedIfIndicated() {
93+
//language=java
94+
rewriteRun(
95+
spec -> spec.recipe(new StringFormatted(false)),
96+
version(
97+
java(
98+
"""
99+
package com.example.app;
100+
class A {
101+
String str = String.format("foo"
102+
+ "%s", "a");
103+
}
104+
"""
81105
),
82106
17
83107
)
@@ -92,21 +116,21 @@ void callingFunction() {
92116
java(
93117
"""
94118
package com.example.app;
95-
119+
96120
class A {
97121
String str = String.format(getTemplateString(), "a");
98-
122+
99123
private String getTemplateString() {
100124
return "foo %s";
101125
}
102126
}
103127
""",
104128
"""
105129
package com.example.app;
106-
130+
107131
class A {
108132
String str = getTemplateString().formatted("a");
109-
133+
110134
private String getTemplateString() {
111135
return "foo %s";
112136
}

0 commit comments

Comments
 (0)