diff --git a/src/main/java/org/openrewrite/java/migrate/io/ReplaceSystemOutWithIOPrint.java b/src/main/java/org/openrewrite/java/migrate/io/ReplaceSystemOutWithIOPrint.java new file mode 100644 index 0000000000..d4c7a28d54 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/io/ReplaceSystemOutWithIOPrint.java @@ -0,0 +1,89 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.io; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesMethod; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.TypeUtils; + +public class ReplaceSystemOutWithIOPrint extends Recipe { + + @Override + public String getDisplayName() { + return "Migrate `System.out.print` to Java 25 IO utility class"; + } + + @Override + public String getDescription() { + return "Replace `System.out.print()`, `System.out.println()` with `IO.print()` and `IO.println()`. " + + "Migrates to the new IO utility class introduced in Java 25."; + } + + private static final MethodMatcher SYSTEM_OUT_PRINT = new MethodMatcher("java.io.PrintStream print(..)"); + private static final MethodMatcher SYSTEM_OUT_PRINTLN = new MethodMatcher("java.io.PrintStream println(..)"); + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.or( + new UsesMethod<>(SYSTEM_OUT_PRINT), + new UsesMethod<>(SYSTEM_OUT_PRINTLN) + ), + new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation m = super.visitMethodInvocation(method, ctx); + if (!isSystemOutMethod(m)) { + return m; + } + String methodName = m.getName().getSimpleName(); + return m.getArguments().isEmpty() ? + JavaTemplate.builder("IO.#{}()").build() + .apply(getCursor(), m.getCoordinates().replace(), methodName) : + JavaTemplate.builder("IO.#{}(#{any()})").build() + .apply(getCursor(), m.getCoordinates().replace(), methodName, m.getArguments().get(0)); + } + + private boolean isSystemOutMethod(J.MethodInvocation mi) { + if (SYSTEM_OUT_PRINT.matches(mi) || SYSTEM_OUT_PRINTLN.matches(mi)) { + Expression expression = mi.getSelect(); + if (expression instanceof J.FieldAccess) { + return isSystemOut(((J.FieldAccess) expression).getName()); + } + if (expression instanceof J.Identifier) { + maybeRemoveImport("java.lang.System.out"); + return isSystemOut((J.Identifier) expression); + } + } + return false; + } + + private boolean isSystemOut(J.Identifier identifier) { + return "out".equals(identifier.getSimpleName()) && + identifier.getFieldType() != null && + TypeUtils.isAssignableTo("java.lang.System", identifier.getFieldType().getOwner()); + } + }); + } +} diff --git a/src/main/resources/META-INF/rewrite/java-version-25.yml b/src/main/resources/META-INF/rewrite/java-version-25.yml index 4891bc6747..5b3f2f4e4d 100644 --- a/src/main/resources/META-INF/rewrite/java-version-25.yml +++ b/src/main/resources/META-INF/rewrite/java-version-25.yml @@ -30,6 +30,7 @@ recipeList: version: 25 - org.openrewrite.github.SetupJavaUpgradeJavaVersion: minimumJavaMajorVersion: 25 + - org.openrewrite.java.migrate.io.ReplaceSystemOutWithIOPrint - org.openrewrite.java.migrate.lang.MigrateProcessWaitForDuration - org.openrewrite.java.migrate.lang.ReplaceUnusedVariablesWithUnderscore - org.openrewrite.java.migrate.util.MigrateInflaterDeflaterToClose diff --git a/src/test/java/org/openrewrite/java/migrate/io/ReplaceSystemOutWithIOPrintTest.java b/src/test/java/org/openrewrite/java/migrate/io/ReplaceSystemOutWithIOPrintTest.java new file mode 100644 index 0000000000..b483b34475 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/io/ReplaceSystemOutWithIOPrintTest.java @@ -0,0 +1,263 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.io; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.search.FindMissingTypes; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.javaVersion; + +class ReplaceSystemOutWithIOPrintTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new ReplaceSystemOutWithIOPrint()) + .afterTypeValidationOptions(TypeValidation.all().allowMissingType(o -> { + assert o instanceof FindMissingTypes.MissingTypeResult; + FindMissingTypes.MissingTypeResult result = (FindMissingTypes.MissingTypeResult) o; + return result.getPrintedTree().contains("IO"); + })) // TODO remove once tests run on Java 25+ + .parser(JavaParser.fromJavaVersion()) + .allSources(s -> s.markers(javaVersion(25))); + } + + @DocumentExample + @Test + void replaceSystemOutPrint() { + rewriteRun( + java( + """ + class Example { + void test() { + System.out.print("Hello"); + } + } + """, + """ + class Example { + void test() { + IO.print("Hello"); + } + } + """ + ) + ); + } + + @Test + void replaceSystemOutPrintln() { + rewriteRun( + java( + """ + class Example { + void test() { + System.out.println("Hello"); + } + } + """, + """ + class Example { + void test() { + IO.println("Hello"); + } + } + """ + ) + ); + } + + @Test + void replaceSystemOutPrintlnWithStaticImport() { + rewriteRun( + java( + """ + import static java.lang.System.out; + + class Example { + void test() { + out.println("Hello"); + } + } + """, + """ + class Example { + void test() { + IO.println("Hello"); + } + } + """ + ) + ); + } + + @Test + void replaceSystemOutPrintWithVariable() { + rewriteRun( + java( + """ + class Example { + void test() { + String message = "Hello World"; + System.out.print(message); + } + } + """, + """ + class Example { + void test() { + String message = "Hello World"; + IO.print(message); + } + } + """ + ) + ); + } + + @Test + void replaceSystemOutPrintlnEmpty() { + rewriteRun( + java( + """ + class Example { + void test() { + System.out.println(); + } + } + """, + """ + class Example { + void test() { + IO.println(); + } + } + """ + ) + ); + } + + @Test + void replaceMultipleSystemOutCalls() { + rewriteRun( + java( + """ + class Example { + void test() { + System.out.print("Hello"); + System.out.println(" World"); + System.out.print(42); + System.out.println(); + } + } + """, + """ + class Example { + void test() { + IO.print("Hello"); + IO.println(" World"); + IO.print(42); + IO.println(); + } + } + """ + ) + ); + } + + @Test + void handlesPrintWithComplexExpressions() { + rewriteRun( + java( + """ + class Example { + void test() { + String name = "John"; + int age = 30; + System.out.print("Name: " + name + ", Age: " + age); + System.out.println(String.format("Formatted: %s is %d years old", name, age)); + } + } + """, + """ + class Example { + void test() { + String name = "John"; + int age = 30; + IO.print("Name: " + name + ", Age: " + age); + IO.println(String.format("Formatted: %s is %d years old", name, age)); + } + } + """ + ) + ); + } + + @Test + void doesNotReplaceSystemErrCalls() { + rewriteRun( + java( + """ + class Example { + void test() { + System.err.print("Error message"); + System.err.println("Error message"); + } + } + """ + ) + ); + } + + @Test + void doesNotReplaceOtherPrintStreams() { + rewriteRun( + java( + """ + import java.io.PrintStream; + + class Example { + void test() { + PrintStream ps = new PrintStream(System.out); + ps.print("Should not change"); + ps.println("Should not change"); + } + } + """ + ) + ); + } + + @Test + void doesNotReplacePrintf() { + rewriteRun( + java( + """ + class Example { + void test() { + System.out.printf("Hello%n"); + } + } + """ + ) + ); + } +}