Skip to content

Commit b566417

Browse files
e5LAtimtebeek
andauthored
Add recipe to migrate System.out.print/println to IO.print/println (#848)
* Add ReplaceSystemOutWithIOPrint to migrate System.out.print/println to IO.print/println * refactor: reformatting * Handle additional edge case with static import * Inline templates * Use markdown * Include with Java 25 upgrade * Do not replace `printf` --------- Co-authored-by: Tim te Beek <[email protected]>
1 parent c25002d commit b566417

File tree

3 files changed

+353
-0
lines changed

3 files changed

+353
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.io;
17+
18+
import org.openrewrite.ExecutionContext;
19+
import org.openrewrite.Preconditions;
20+
import org.openrewrite.Recipe;
21+
import org.openrewrite.TreeVisitor;
22+
import org.openrewrite.java.JavaIsoVisitor;
23+
import org.openrewrite.java.JavaTemplate;
24+
import org.openrewrite.java.MethodMatcher;
25+
import org.openrewrite.java.search.UsesMethod;
26+
import org.openrewrite.java.tree.Expression;
27+
import org.openrewrite.java.tree.J;
28+
import org.openrewrite.java.tree.TypeUtils;
29+
30+
public class ReplaceSystemOutWithIOPrint extends Recipe {
31+
32+
@Override
33+
public String getDisplayName() {
34+
return "Migrate `System.out.print` to Java 25 IO utility class";
35+
}
36+
37+
@Override
38+
public String getDescription() {
39+
return "Replace `System.out.print()`, `System.out.println()` with `IO.print()` and `IO.println()`. " +
40+
"Migrates to the new IO utility class introduced in Java 25.";
41+
}
42+
43+
private static final MethodMatcher SYSTEM_OUT_PRINT = new MethodMatcher("java.io.PrintStream print(..)");
44+
private static final MethodMatcher SYSTEM_OUT_PRINTLN = new MethodMatcher("java.io.PrintStream println(..)");
45+
46+
@Override
47+
public TreeVisitor<?, ExecutionContext> getVisitor() {
48+
return Preconditions.check(
49+
Preconditions.or(
50+
new UsesMethod<>(SYSTEM_OUT_PRINT),
51+
new UsesMethod<>(SYSTEM_OUT_PRINTLN)
52+
),
53+
new JavaIsoVisitor<ExecutionContext>() {
54+
@Override
55+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
56+
J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
57+
if (!isSystemOutMethod(m)) {
58+
return m;
59+
}
60+
String methodName = m.getName().getSimpleName();
61+
return m.getArguments().isEmpty() ?
62+
JavaTemplate.builder("IO.#{}()").build()
63+
.apply(getCursor(), m.getCoordinates().replace(), methodName) :
64+
JavaTemplate.builder("IO.#{}(#{any()})").build()
65+
.apply(getCursor(), m.getCoordinates().replace(), methodName, m.getArguments().get(0));
66+
}
67+
68+
private boolean isSystemOutMethod(J.MethodInvocation mi) {
69+
if (SYSTEM_OUT_PRINT.matches(mi) || SYSTEM_OUT_PRINTLN.matches(mi)) {
70+
Expression expression = mi.getSelect();
71+
if (expression instanceof J.FieldAccess) {
72+
return isSystemOut(((J.FieldAccess) expression).getName());
73+
}
74+
if (expression instanceof J.Identifier) {
75+
maybeRemoveImport("java.lang.System.out");
76+
return isSystemOut((J.Identifier) expression);
77+
}
78+
}
79+
return false;
80+
}
81+
82+
private boolean isSystemOut(J.Identifier identifier) {
83+
return "out".equals(identifier.getSimpleName()) &&
84+
identifier.getFieldType() != null &&
85+
TypeUtils.isAssignableTo("java.lang.System", identifier.getFieldType().getOwner());
86+
}
87+
});
88+
}
89+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ recipeList:
3030
version: 25
3131
- org.openrewrite.github.SetupJavaUpgradeJavaVersion:
3232
minimumJavaMajorVersion: 25
33+
- org.openrewrite.java.migrate.io.ReplaceSystemOutWithIOPrint
3334
- org.openrewrite.java.migrate.lang.MigrateProcessWaitForDuration
3435
- org.openrewrite.java.migrate.lang.ReplaceUnusedVariablesWithUnderscore
3536
- org.openrewrite.java.migrate.util.MigrateInflaterDeflaterToClose
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.io;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.openrewrite.DocumentExample;
20+
import org.openrewrite.java.JavaParser;
21+
import org.openrewrite.java.search.FindMissingTypes;
22+
import org.openrewrite.test.RecipeSpec;
23+
import org.openrewrite.test.RewriteTest;
24+
import org.openrewrite.test.TypeValidation;
25+
26+
import static org.openrewrite.java.Assertions.java;
27+
import static org.openrewrite.java.Assertions.javaVersion;
28+
29+
class ReplaceSystemOutWithIOPrintTest implements RewriteTest {
30+
31+
@Override
32+
public void defaults(RecipeSpec spec) {
33+
spec.recipe(new ReplaceSystemOutWithIOPrint())
34+
.afterTypeValidationOptions(TypeValidation.all().allowMissingType(o -> {
35+
assert o instanceof FindMissingTypes.MissingTypeResult;
36+
FindMissingTypes.MissingTypeResult result = (FindMissingTypes.MissingTypeResult) o;
37+
return result.getPrintedTree().contains("IO");
38+
})) // TODO remove once tests run on Java 25+
39+
.parser(JavaParser.fromJavaVersion())
40+
.allSources(s -> s.markers(javaVersion(25)));
41+
}
42+
43+
@DocumentExample
44+
@Test
45+
void replaceSystemOutPrint() {
46+
rewriteRun(
47+
java(
48+
"""
49+
class Example {
50+
void test() {
51+
System.out.print("Hello");
52+
}
53+
}
54+
""",
55+
"""
56+
class Example {
57+
void test() {
58+
IO.print("Hello");
59+
}
60+
}
61+
"""
62+
)
63+
);
64+
}
65+
66+
@Test
67+
void replaceSystemOutPrintln() {
68+
rewriteRun(
69+
java(
70+
"""
71+
class Example {
72+
void test() {
73+
System.out.println("Hello");
74+
}
75+
}
76+
""",
77+
"""
78+
class Example {
79+
void test() {
80+
IO.println("Hello");
81+
}
82+
}
83+
"""
84+
)
85+
);
86+
}
87+
88+
@Test
89+
void replaceSystemOutPrintlnWithStaticImport() {
90+
rewriteRun(
91+
java(
92+
"""
93+
import static java.lang.System.out;
94+
95+
class Example {
96+
void test() {
97+
out.println("Hello");
98+
}
99+
}
100+
""",
101+
"""
102+
class Example {
103+
void test() {
104+
IO.println("Hello");
105+
}
106+
}
107+
"""
108+
)
109+
);
110+
}
111+
112+
@Test
113+
void replaceSystemOutPrintWithVariable() {
114+
rewriteRun(
115+
java(
116+
"""
117+
class Example {
118+
void test() {
119+
String message = "Hello World";
120+
System.out.print(message);
121+
}
122+
}
123+
""",
124+
"""
125+
class Example {
126+
void test() {
127+
String message = "Hello World";
128+
IO.print(message);
129+
}
130+
}
131+
"""
132+
)
133+
);
134+
}
135+
136+
@Test
137+
void replaceSystemOutPrintlnEmpty() {
138+
rewriteRun(
139+
java(
140+
"""
141+
class Example {
142+
void test() {
143+
System.out.println();
144+
}
145+
}
146+
""",
147+
"""
148+
class Example {
149+
void test() {
150+
IO.println();
151+
}
152+
}
153+
"""
154+
)
155+
);
156+
}
157+
158+
@Test
159+
void replaceMultipleSystemOutCalls() {
160+
rewriteRun(
161+
java(
162+
"""
163+
class Example {
164+
void test() {
165+
System.out.print("Hello");
166+
System.out.println(" World");
167+
System.out.print(42);
168+
System.out.println();
169+
}
170+
}
171+
""",
172+
"""
173+
class Example {
174+
void test() {
175+
IO.print("Hello");
176+
IO.println(" World");
177+
IO.print(42);
178+
IO.println();
179+
}
180+
}
181+
"""
182+
)
183+
);
184+
}
185+
186+
@Test
187+
void handlesPrintWithComplexExpressions() {
188+
rewriteRun(
189+
java(
190+
"""
191+
class Example {
192+
void test() {
193+
String name = "John";
194+
int age = 30;
195+
System.out.print("Name: " + name + ", Age: " + age);
196+
System.out.println(String.format("Formatted: %s is %d years old", name, age));
197+
}
198+
}
199+
""",
200+
"""
201+
class Example {
202+
void test() {
203+
String name = "John";
204+
int age = 30;
205+
IO.print("Name: " + name + ", Age: " + age);
206+
IO.println(String.format("Formatted: %s is %d years old", name, age));
207+
}
208+
}
209+
"""
210+
)
211+
);
212+
}
213+
214+
@Test
215+
void doesNotReplaceSystemErrCalls() {
216+
rewriteRun(
217+
java(
218+
"""
219+
class Example {
220+
void test() {
221+
System.err.print("Error message");
222+
System.err.println("Error message");
223+
}
224+
}
225+
"""
226+
)
227+
);
228+
}
229+
230+
@Test
231+
void doesNotReplaceOtherPrintStreams() {
232+
rewriteRun(
233+
java(
234+
"""
235+
import java.io.PrintStream;
236+
237+
class Example {
238+
void test() {
239+
PrintStream ps = new PrintStream(System.out);
240+
ps.print("Should not change");
241+
ps.println("Should not change");
242+
}
243+
}
244+
"""
245+
)
246+
);
247+
}
248+
249+
@Test
250+
void doesNotReplacePrintf() {
251+
rewriteRun(
252+
java(
253+
"""
254+
class Example {
255+
void test() {
256+
System.out.printf("Hello%n");
257+
}
258+
}
259+
"""
260+
)
261+
);
262+
}
263+
}

0 commit comments

Comments
 (0)