diff --git a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java new file mode 100644 index 0000000000..602ea572eb --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java @@ -0,0 +1,123 @@ +/* + * 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.util; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.search.UsesMethod; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; + +@EqualsAndHashCode(callSuper = false) +@Value +public class MigrateStringReaderToReaderOf extends Recipe { + private static final MethodMatcher STRING_READER_CONSTRUCTOR = new MethodMatcher("java.io.StringReader (java.lang.String)"); + private static final MethodMatcher TO_STRING_METHOD = new MethodMatcher("java.lang.Object toString()", true); + + @Override + public String getDisplayName() { + return "Use `Reader.of(CharSequence)` for non-synchronized readers"; + } + + @Override + public String getDescription() { + return "Migrate `new StringReader(String)` to `Reader.of(CharSequence)` in Java 25+. " + + "This only applies when assigning to `Reader` variables or returning from methods that return `Reader`. " + + "The new method creates non-synchronized readers which are more efficient when thread-safety is not required."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.and(new UsesJavaVersion<>(25), new UsesMethod<>(STRING_READER_CONSTRUCTOR)), + new JavaVisitor() { + @Override + public J visitVariableDeclarations(J.VariableDeclarations mV, ExecutionContext ctx) { + if (TypeUtils.isOfClassType(mV.getTypeAsFullyQualified(), "java.io.Reader")) { + return mV.withVariables(ListUtils.map(mV.getVariables(), v -> { + maybeRemoveImport("java.io.StringReader"); + maybeAddImport("java.io.Reader"); + return (J.VariableDeclarations.NamedVariable) new TransformVisitor().visitNonNull(v, ctx, getCursor().getParentOrThrow()); + })); + } + return super.visitVariableDeclarations(mV, ctx); + } + + @Override + public J visitAssignment(J.Assignment a, ExecutionContext ctx) { + if (a.getVariable() instanceof J.Identifier) { + J.Identifier variable = (J.Identifier) a.getVariable(); + if (TypeUtils.isOfClassType(variable.getType(), "java.io.Reader")) { + maybeRemoveImport("java.io.StringReader"); + maybeAddImport("java.io.Reader"); + return new TransformVisitor().visitNonNull(a, ctx, getCursor().getParentOrThrow()); + } + } + return super.visitAssignment(a, ctx); + } + + @Override + public J visitReturn(J.Return r, ExecutionContext ctx) { + J.MethodDeclaration method = getCursor().firstEnclosing(J.MethodDeclaration.class); + if (method != null && method.getReturnTypeExpression() != null) { + JavaType returnType = method.getReturnTypeExpression().getType(); + if (TypeUtils.isOfClassType(returnType, "java.io.Reader")) { + maybeRemoveImport("java.io.StringReader"); + maybeAddImport("java.io.Reader"); + return new TransformVisitor().visitNonNull(r, ctx, getCursor().getParentOrThrow()); + } + } + return super.visitReturn(r, ctx); + } + } + ); + } + + private static class TransformVisitor extends JavaVisitor { + @Override + public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) { + if (STRING_READER_CONSTRUCTOR.matches(newClass)) { + return JavaTemplate.builder("Reader.of(#{any(java.lang.CharSequence)})") + .imports("java.io.Reader") + .build() + .apply(getCursor(), newClass.getCoordinates().replace(), optimizeCharSequenceToString(newClass.getArguments().get(0))); + } + return super.visitNewClass(newClass, ctx); + } + + private Expression optimizeCharSequenceToString(Expression expr) { + if (expr instanceof J.MethodInvocation) { + J.MethodInvocation mi = (J.MethodInvocation) expr; + if (TO_STRING_METHOD.matches(mi) && + mi.getSelect() != null && TypeUtils.isAssignableTo("java.lang.CharSequence", mi.getSelect().getType())) { + return mi.getSelect(); + } + } + return expr; + } + } +} 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 1bc043cf76..02fb580696 100644 --- a/src/main/resources/META-INF/rewrite/java-version-25.yml +++ b/src/main/resources/META-INF/rewrite/java-version-25.yml @@ -29,6 +29,7 @@ recipeList: - org.openrewrite.java.migrate.UpgradeBuildToJava25 - org.openrewrite.java.migrate.lang.MigrateProcessWaitForDuration - org.openrewrite.java.migrate.util.MigrateInflaterDeflaterToClose + - org.openrewrite.java.migrate.util.MigrateStringReaderToReaderOf - org.openrewrite.java.migrate.AccessController - org.openrewrite.java.migrate.RemoveSecurityPolicy - org.openrewrite.java.migrate.RemoveSecurityManager diff --git a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java new file mode 100644 index 0000000000..6380eb05a0 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java @@ -0,0 +1,393 @@ +/* + * 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.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +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 MigrateStringReaderToReaderOfTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new MigrateStringReaderToReaderOf()) + .afterTypeValidationOptions(TypeValidation.all().methodInvocations(false)) // Until we run tests on Java 25+ + .parser(JavaParser.fromJavaVersion()) + .allSources(s -> s.markers(javaVersion(25))); + } + + @DocumentExample + @Test + void migrateReaderVariableWithString() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + void test(String content) { + Reader reader = new StringReader(content); + } + } + """, + """ + import java.io.Reader; + + class Test { + void test(String content) { + Reader reader = Reader.of(content); + } + } + """ + ) + ); + } + + @Test + void doNotMigrateStringReaderVariable() { + rewriteRun( + //language=java + java( + """ + import java.io.StringReader; + + class Test { + void test(String content) { + StringReader reader = new StringReader(content); + } + } + """ + ) + ); + } + + @Test + void migrateMethodReturningReader() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + Reader createReader(String content) { + return new StringReader(content); + } + } + """, + """ + import java.io.Reader; + + class Test { + Reader createReader(String content) { + return Reader.of(content); + } + } + """ + ) + ); + } + + @Test + void doNotMigrateMethodReturningStringReader() { + rewriteRun( + //language=java + java( + """ + import java.io.StringReader; + + class Test { + StringReader createReader(String content) { + return new StringReader(content); + } + } + """ + ) + ); + } + + @ParameterizedTest + @ValueSource(strings = {"StringBuilder", "StringBuffer", "CharBuffer", "CharSequence"}) + void migrateCharSequenceVariants(String className) { + String extraImport = "CharBuffer".equals(className) ? "\nimport java.nio.CharBuffer;" : ""; + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader;%s + + class Test { + void test(%s cs) { + Reader reader = new StringReader(cs.toString()); + } + } + """.formatted(extraImport, className), + """ + import java.io.Reader;%s + + class Test { + void test(%s cs) { + Reader reader = Reader.of(cs); + } + } + """.formatted(extraImport, className) + ); + } + + @Test + void migrateMultipleReaderVariables() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + void test(String s1, String s2) { + Reader reader1 = new StringReader(s1); + Reader reader2 = new StringReader(s2); + } + } + """, + """ + import java.io.Reader; + + class Test { + void test(String s1, String s2) { + Reader reader1 = Reader.of(s1); + Reader reader2 = Reader.of(s2); + } + } + """ + ) + ); + } + + @Test + void doNotMigrateAsMethodArgument() { + rewriteRun( + //language=java + java( + """ + import java.io.BufferedReader; + import java.io.StringReader; + + class Test { + void test(String content) { + BufferedReader br = new BufferedReader(new StringReader(content)); + } + } + """ + ) + ); + } + + @Test + void migrateInTryWithResources() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + void test(String content) throws Exception { + try (Reader reader = new StringReader(content)) { + // use reader + } + } + } + """, + """ + import java.io.Reader; + + class Test { + void test(String content) throws Exception { + try (Reader reader = Reader.of(content)) { + // use reader + } + } + } + """ + ) + ); + } + + @Test + void migrateWithLiteral() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + void test() { + Reader reader = new StringReader("Hello World"); + } + } + """, + """ + import java.io.Reader; + + class Test { + void test() { + Reader reader = Reader.of("Hello World"); + } + } + """ + ) + ); + } + + @Test + void migrateReaderFieldAssignment() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + private Reader reader; + + void test(String content) { + reader = new StringReader(content); + } + } + """, + """ + import java.io.Reader; + + class Test { + private Reader reader; + + void test(String content) { + reader = Reader.of(content); + } + } + """ + ) + ); + } + + @Test + void doNotMigrateBeforeJava25() { + rewriteRun( + spec -> spec.allSources(s -> s.markers(javaVersion(24))), + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + void test(String content) { + Reader reader = new StringReader(content); + } + } + """ + ) + ); + } + + @Test + void migrateComplexReturn() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + Reader getReader(boolean flag, String s1, String s2) { + if (flag) { + return new StringReader(s1); + } else { + return new StringReader(s2); + } + } + } + """, + """ + import java.io.Reader; + + class Test { + Reader getReader(boolean flag, String s1, String s2) { + if (flag) { + return Reader.of(s1); + } else { + return Reader.of(s2); + } + } + } + """ + ) + ); + } + + @Test + void doNotMigrateLambdaReturn() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + import java.util.function.Function; + + class Test { + Function factory = s -> new StringReader(s); + } + """ + ) + ); + } + + @Test + void doNotMigrateMethodReference() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + import java.util.function.Function; + + class Test { + Function factory = StringReader::new; + } + """ + ) + ); + } +}