From 8d42eecbd96e78cffb159dca8a2dff033ddd5729 Mon Sep 17 00:00:00 2001 From: Johan Kragt Date: Wed, 10 Sep 2025 11:33:41 +0200 Subject: [PATCH 01/17] Setup --- .../util/MigrateStringReaderToReaderOf.java | 198 ++++++++ .../META-INF/rewrite/java-version-25.yml | 1 + .../MigrateStringReaderToReaderOfTest.java | 475 ++++++++++++++++++ 3 files changed, 674 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java create mode 100644 src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java 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..ed1c9282fb --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java @@ -0,0 +1,198 @@ +/* + * 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.JavaIsoVisitor; +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.UsesType; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.ArrayList; + +/** + * Migrates StringReader to Reader.of(CharSequence) for Java 25+. + * This recipe only transforms: + * 1. Assignments to variables of type Reader (not StringReader) + * 2. Return statements from methods that return Reader (not StringReader) + */ +@EqualsAndHashCode(callSuper = false) +@Value +public class MigrateStringReaderToReaderOf extends Recipe { + private static final MethodMatcher STRING_READER_CONSTRUCTOR = new MethodMatcher("java.io.StringReader (java.lang.String)"); + + @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 UsesType<>("java.io.StringReader", false) + ), + new JavaVisitor() { + + @Override + public J visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext executionContext) { + // Only process if the variable type is Reader (not StringReader) + if (isReaderType(multiVariable.getTypeAsFullyQualified())) { + // Check each variable in the declaration + return multiVariable.withVariables(ListUtils.map(multiVariable.getVariables(), v -> { + Expression initializer = v.getInitializer(); + if (initializer instanceof J.NewClass) { + J.NewClass nc = (J.NewClass) initializer; + if (STRING_READER_CONSTRUCTOR.matches(nc)) { + maybeRemoveImport("java.io.StringReader"); + return (J.VariableDeclarations.NamedVariable) new TransformVisitor().visit(v, executionContext, getCursor().getParent()); + } + } + return v; + })); + } + return super.visitVariableDeclarations(multiVariable, executionContext); + } + + @Override + public J visitAssignment(J.Assignment assignment, ExecutionContext executionContext) { + // Check if assigning new StringReader to a Reader variable + if (assignment.getAssignment() instanceof J.NewClass) { + J.NewClass nc = (J.NewClass) assignment.getAssignment(); + if (STRING_READER_CONSTRUCTOR.matches(nc)) { + // Check if the variable being assigned to is of type Reader + if (assignment.getVariable() instanceof J.Identifier) { + J.Identifier variable = (J.Identifier) assignment.getVariable(); + if (isReaderType(variable.getType())) { + maybeRemoveImport("java.io.StringReader"); + return new TransformVisitor().visit(assignment, executionContext, getCursor().getParent()); + } + } + } + } + return super.visitAssignment(assignment, executionContext); + } + + @Override + public J visitReturn(J.Return return_, ExecutionContext executionContext) { + // Check if returning new StringReader from a method that returns Reader + if (return_.getExpression() instanceof J.NewClass) { + J.NewClass nc = (J.NewClass) return_.getExpression(); + if (STRING_READER_CONSTRUCTOR.matches(nc)) { + // Check if the method returns Reader (not StringReader) + J.MethodDeclaration method = getCursor().firstEnclosing(J.MethodDeclaration.class); + if (method != null && method.getReturnTypeExpression() != null) { + JavaType returnType = method.getReturnTypeExpression().getType(); + if (isReaderType(returnType)) { + maybeRemoveImport("java.io.StringReader"); + return new TransformVisitor().visit(return_, executionContext, getCursor().getParent()); + } + } + } + } + return super.visitReturn(return_, executionContext); + } + + private boolean isReaderType(JavaType type) { + if (type instanceof JavaType.FullyQualified) { + return "java.io.Reader".equals(((JavaType.FullyQualified) type).getFullyQualifiedName()); + } + return false; + } + + private class TransformVisitor extends JavaVisitor { + @Override + public J visitNewClass(J.NewClass newClass, ExecutionContext executionContext) { + if (STRING_READER_CONSTRUCTOR.matches(newClass)) { + Expression argument = newClass.getArguments().get(0); + + // Optimize CharSequence.toString() calls + argument = optimizeCharSequenceToString(argument); + + JavaTemplate template = JavaTemplate.builder("Reader.of(#{any(java.lang.CharSequence)})") + .imports("java.io.Reader") + .contextSensitive() + .build(); + + maybeAddImport("java.io.Reader"); + return template.apply(getCursor(), newClass.getCoordinates().replace(), argument); + } + return super.visitNewClass(newClass, executionContext); + } + + private Expression optimizeCharSequenceToString(Expression expr) { + if (expr instanceof J.MethodInvocation) { + J.MethodInvocation mi = (J.MethodInvocation) expr; + if ("toString".equals(mi.getSimpleName()) && + (mi.getArguments().isEmpty() || (mi.getArguments().size() == 1 && mi.getArguments().get(0) instanceof J.Empty)) && + mi.getSelect() != null && + isCharSequenceType(mi.getSelect().getType())) { + return mi.getSelect(); + } + } + return expr; + } + + private boolean isCharSequenceType(JavaType type) { + if (type instanceof JavaType.Class) { + JavaType.Class classType = (JavaType.Class) type; + String fqn = classType.getFullyQualifiedName(); + return "java.lang.CharSequence".equals(fqn) || + "java.lang.String".equals(fqn) || + "java.lang.StringBuilder".equals(fqn) || + "java.lang.StringBuffer".equals(fqn) || + "java.nio.CharBuffer".equals(fqn) || + implementsCharSequence(classType); + } + return false; + } + + private boolean implementsCharSequence(JavaType.Class classType) { + for (JavaType.FullyQualified iface : classType.getInterfaces()) { + if ("java.lang.CharSequence".equals(iface.getFullyQualifiedName())) { + return true; + } + } + JavaType.FullyQualified supertype = classType.getSupertype(); + if (supertype instanceof JavaType.Class) { + return implementsCharSequence((JavaType.Class) supertype); + } + return false; + } + } + } + ); + } +} 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..a78959ab13 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java @@ -0,0 +1,475 @@ +/* + * 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.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +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()) + .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); + } + } + """ + ) + ); + } + + @Test + void migrateWithStringBuilder() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + void test(StringBuilder sb) { + Reader reader = new StringReader(sb.toString()); + } + } + """, + """ + import java.io.Reader; + + class Test { + void test(StringBuilder sb) { + Reader reader = Reader.of(sb); + } + } + """ + ) + ); + } + + @Test + void migrateWithStringBuffer() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + void test(StringBuffer sb) { + Reader reader = new StringReader(sb.toString()); + } + } + """, + """ + import java.io.Reader; + + class Test { + void test(StringBuffer sb) { + Reader reader = Reader.of(sb); + } + } + """ + ) + ); + } + + @Test + void migrateWithCharBuffer() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + import java.nio.CharBuffer; + + class Test { + void test(CharBuffer cb) { + Reader reader = new StringReader(cb.toString()); + } + } + """, + """ + import java.io.Reader; + import java.nio.CharBuffer; + + class Test { + void test(CharBuffer cb) { + Reader reader = Reader.of(cb); + } + } + """ + ) + ); + } + + @Test + void migrateWithCharSequence() { + rewriteRun( + //language=java + java( + """ + import java.io.Reader; + import java.io.StringReader; + + class Test { + void test(CharSequence cs) { + Reader reader = new StringReader(cs.toString()); + } + } + """, + """ + import java.io.Reader; + + class Test { + void test(CharSequence cs) { + Reader reader = Reader.of(cs); + } + } + """ + ) + ); + } + + @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; + } + """ + ) + ); + } +} From baa1537c3fd3e611b80fd2e6364e39f8384f2857 Mon Sep 17 00:00:00 2001 From: Johan Kragt Date: Wed, 10 Sep 2025 13:47:48 +0200 Subject: [PATCH 02/17] polish --- .../util/MigrateStringReaderToReaderOf.java | 109 ++++-------------- 1 file changed, 21 insertions(+), 88 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java index ed1c9282fb..c2372e573b 100644 --- a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java +++ b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java @@ -22,7 +22,6 @@ import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; import org.openrewrite.internal.ListUtils; -import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.JavaTemplate; import org.openrewrite.java.JavaVisitor; import org.openrewrite.java.MethodMatcher; @@ -31,15 +30,8 @@ import org.openrewrite.java.tree.Expression; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; -import java.util.ArrayList; - -/** - * Migrates StringReader to Reader.of(CharSequence) for Java 25+. - * This recipe only transforms: - * 1. Assignments to variables of type Reader (not StringReader) - * 2. Return statements from methods that return Reader (not StringReader) - */ @EqualsAndHashCode(callSuper = false) @Value public class MigrateStringReaderToReaderOf extends Recipe { @@ -53,8 +45,8 @@ public String getDisplayName() { @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."; + "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 @@ -68,19 +60,11 @@ public TreeVisitor getVisitor() { @Override public J visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext executionContext) { - // Only process if the variable type is Reader (not StringReader) - if (isReaderType(multiVariable.getTypeAsFullyQualified())) { - // Check each variable in the declaration + if (TypeUtils.isOfClassType(multiVariable.getTypeAsFullyQualified(), "java.io.Reader")) { return multiVariable.withVariables(ListUtils.map(multiVariable.getVariables(), v -> { - Expression initializer = v.getInitializer(); - if (initializer instanceof J.NewClass) { - J.NewClass nc = (J.NewClass) initializer; - if (STRING_READER_CONSTRUCTOR.matches(nc)) { - maybeRemoveImport("java.io.StringReader"); - return (J.VariableDeclarations.NamedVariable) new TransformVisitor().visit(v, executionContext, getCursor().getParent()); - } - } - return v; + maybeRemoveImport("java.io.StringReader"); + maybeAddImport("java.io.Reader"); + return (J.VariableDeclarations.NamedVariable) new TransformVisitor().visit(v, executionContext, getCursor().getParent()); })); } return super.visitVariableDeclarations(multiVariable, executionContext); @@ -88,18 +72,12 @@ public J visitVariableDeclarations(J.VariableDeclarations multiVariable, Executi @Override public J visitAssignment(J.Assignment assignment, ExecutionContext executionContext) { - // Check if assigning new StringReader to a Reader variable - if (assignment.getAssignment() instanceof J.NewClass) { - J.NewClass nc = (J.NewClass) assignment.getAssignment(); - if (STRING_READER_CONSTRUCTOR.matches(nc)) { - // Check if the variable being assigned to is of type Reader - if (assignment.getVariable() instanceof J.Identifier) { - J.Identifier variable = (J.Identifier) assignment.getVariable(); - if (isReaderType(variable.getType())) { - maybeRemoveImport("java.io.StringReader"); - return new TransformVisitor().visit(assignment, executionContext, getCursor().getParent()); - } - } + if (assignment.getVariable() instanceof J.Identifier) { + J.Identifier variable = (J.Identifier) assignment.getVariable(); + if (TypeUtils.isOfClassType(variable.getType(), "java.io.Reader")) { + maybeRemoveImport("java.io.StringReader"); + maybeAddImport("java.io.Reader"); + return new TransformVisitor().visit(assignment, executionContext, getCursor().getParent()); } } return super.visitAssignment(assignment, executionContext); @@ -107,46 +85,29 @@ public J visitAssignment(J.Assignment assignment, ExecutionContext executionCont @Override public J visitReturn(J.Return return_, ExecutionContext executionContext) { - // Check if returning new StringReader from a method that returns Reader - if (return_.getExpression() instanceof J.NewClass) { - J.NewClass nc = (J.NewClass) return_.getExpression(); - if (STRING_READER_CONSTRUCTOR.matches(nc)) { - // Check if the method returns Reader (not StringReader) - J.MethodDeclaration method = getCursor().firstEnclosing(J.MethodDeclaration.class); - if (method != null && method.getReturnTypeExpression() != null) { - JavaType returnType = method.getReturnTypeExpression().getType(); - if (isReaderType(returnType)) { - maybeRemoveImport("java.io.StringReader"); - return new TransformVisitor().visit(return_, executionContext, getCursor().getParent()); - } - } + 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().visit(return_, executionContext, getCursor().getParent()); } } return super.visitReturn(return_, executionContext); } - private boolean isReaderType(JavaType type) { - if (type instanceof JavaType.FullyQualified) { - return "java.io.Reader".equals(((JavaType.FullyQualified) type).getFullyQualifiedName()); - } - return false; - } - private class TransformVisitor extends JavaVisitor { @Override public J visitNewClass(J.NewClass newClass, ExecutionContext executionContext) { if (STRING_READER_CONSTRUCTOR.matches(newClass)) { Expression argument = newClass.getArguments().get(0); - - // Optimize CharSequence.toString() calls argument = optimizeCharSequenceToString(argument); - JavaTemplate template = JavaTemplate.builder("Reader.of(#{any(java.lang.CharSequence)})") .imports("java.io.Reader") .contextSensitive() .build(); - maybeAddImport("java.io.Reader"); return template.apply(getCursor(), newClass.getCoordinates().replace(), argument); } return super.visitNewClass(newClass, executionContext); @@ -157,40 +118,12 @@ private Expression optimizeCharSequenceToString(Expression expr) { J.MethodInvocation mi = (J.MethodInvocation) expr; if ("toString".equals(mi.getSimpleName()) && (mi.getArguments().isEmpty() || (mi.getArguments().size() == 1 && mi.getArguments().get(0) instanceof J.Empty)) && - mi.getSelect() != null && - isCharSequenceType(mi.getSelect().getType())) { + mi.getSelect() != null && TypeUtils.isAssignableTo("java.lang.CharSequence", mi.getSelect().getType())) { return mi.getSelect(); } } return expr; } - - private boolean isCharSequenceType(JavaType type) { - if (type instanceof JavaType.Class) { - JavaType.Class classType = (JavaType.Class) type; - String fqn = classType.getFullyQualifiedName(); - return "java.lang.CharSequence".equals(fqn) || - "java.lang.String".equals(fqn) || - "java.lang.StringBuilder".equals(fqn) || - "java.lang.StringBuffer".equals(fqn) || - "java.nio.CharBuffer".equals(fqn) || - implementsCharSequence(classType); - } - return false; - } - - private boolean implementsCharSequence(JavaType.Class classType) { - for (JavaType.FullyQualified iface : classType.getInterfaces()) { - if ("java.lang.CharSequence".equals(iface.getFullyQualifiedName())) { - return true; - } - } - JavaType.FullyQualified supertype = classType.getSupertype(); - if (supertype instanceof JavaType.Class) { - return implementsCharSequence((JavaType.Class) supertype); - } - return false; - } } } ); From ba8ba6e4e678c26bdc2937a9f21152ef1e83ffcb Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 10 Sep 2025 14:01:46 +0200 Subject: [PATCH 03/17] Use `ctx` consistently --- .../util/MigrateStringReaderToReaderOf.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java index c2372e573b..76796359b5 100644 --- a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java +++ b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java @@ -59,15 +59,15 @@ public TreeVisitor getVisitor() { new JavaVisitor() { @Override - public J visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext executionContext) { + public J visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { if (TypeUtils.isOfClassType(multiVariable.getTypeAsFullyQualified(), "java.io.Reader")) { return multiVariable.withVariables(ListUtils.map(multiVariable.getVariables(), v -> { maybeRemoveImport("java.io.StringReader"); maybeAddImport("java.io.Reader"); - return (J.VariableDeclarations.NamedVariable) new TransformVisitor().visit(v, executionContext, getCursor().getParent()); + return (J.VariableDeclarations.NamedVariable) new TransformVisitor().visit(v, ctx, getCursor().getParent()); })); } - return super.visitVariableDeclarations(multiVariable, executionContext); + return super.visitVariableDeclarations(multiVariable, ctx); } @Override @@ -84,22 +84,22 @@ public J visitAssignment(J.Assignment assignment, ExecutionContext executionCont } @Override - public J visitReturn(J.Return return_, ExecutionContext executionContext) { + public J visitReturn(J.Return return_, 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().visit(return_, executionContext, getCursor().getParent()); + return new TransformVisitor().visit(return_, ctx, getCursor().getParent()); } } - return super.visitReturn(return_, executionContext); + return super.visitReturn(return_, ctx); } private class TransformVisitor extends JavaVisitor { @Override - public J visitNewClass(J.NewClass newClass, ExecutionContext executionContext) { + public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) { if (STRING_READER_CONSTRUCTOR.matches(newClass)) { Expression argument = newClass.getArguments().get(0); argument = optimizeCharSequenceToString(argument); @@ -110,7 +110,7 @@ public J visitNewClass(J.NewClass newClass, ExecutionContext executionContext) { return template.apply(getCursor(), newClass.getCoordinates().replace(), argument); } - return super.visitNewClass(newClass, executionContext); + return super.visitNewClass(newClass, ctx); } private Expression optimizeCharSequenceToString(Expression expr) { From 634cd66b39f40a4682c4e0c9f2e42f00d983c112 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 10 Sep 2025 14:04:55 +0200 Subject: [PATCH 04/17] Use `ctx` in `visitAssignment` --- .../java/migrate/util/MigrateStringReaderToReaderOf.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java index 76796359b5..343a633946 100644 --- a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java +++ b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java @@ -71,16 +71,16 @@ public J visitVariableDeclarations(J.VariableDeclarations multiVariable, Executi } @Override - public J visitAssignment(J.Assignment assignment, ExecutionContext executionContext) { + public J visitAssignment(J.Assignment assignment, ExecutionContext ctx) { if (assignment.getVariable() instanceof J.Identifier) { J.Identifier variable = (J.Identifier) assignment.getVariable(); if (TypeUtils.isOfClassType(variable.getType(), "java.io.Reader")) { maybeRemoveImport("java.io.StringReader"); maybeAddImport("java.io.Reader"); - return new TransformVisitor().visit(assignment, executionContext, getCursor().getParent()); + return new TransformVisitor().visit(assignment, ctx, getCursor().getParent()); } } - return super.visitAssignment(assignment, executionContext); + return super.visitAssignment(assignment, ctx); } @Override From e5b47da8316c38f43a1f98d2f7b676ba5364f3fb Mon Sep 17 00:00:00 2001 From: Johan Kragt Date: Wed, 10 Sep 2025 14:06:59 +0200 Subject: [PATCH 05/17] polish --- .../util/MigrateStringReaderToReaderOf.java | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java index 343a633946..f2513eb281 100644 --- a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java +++ b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java @@ -36,6 +36,7 @@ @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() { @@ -59,42 +60,42 @@ public TreeVisitor getVisitor() { new JavaVisitor() { @Override - public J visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { - if (TypeUtils.isOfClassType(multiVariable.getTypeAsFullyQualified(), "java.io.Reader")) { - return multiVariable.withVariables(ListUtils.map(multiVariable.getVariables(), v -> { + 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().visit(v, ctx, getCursor().getParent()); + return (J.VariableDeclarations.NamedVariable) new TransformVisitor().visitNonNull(v, ctx, getCursor().getParent()); })); } - return super.visitVariableDeclarations(multiVariable, ctx); + return super.visitVariableDeclarations(mV, ctx); } @Override - public J visitAssignment(J.Assignment assignment, ExecutionContext ctx) { - if (assignment.getVariable() instanceof J.Identifier) { - J.Identifier variable = (J.Identifier) assignment.getVariable(); + 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().visit(assignment, ctx, getCursor().getParent()); + return new TransformVisitor().visitNonNull(a, ctx, getCursor().getParent()); } } - return super.visitAssignment(assignment, ctx); + return super.visitAssignment(a, ctx); } @Override - public J visitReturn(J.Return return_, ExecutionContext ctx) { + 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().visit(return_, ctx, getCursor().getParent()); + return new TransformVisitor().visitNonNull(r, ctx, getCursor().getParent()); } } - return super.visitReturn(return_, ctx); + return super.visitReturn(r, ctx); } private class TransformVisitor extends JavaVisitor { @@ -105,7 +106,6 @@ public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) { argument = optimizeCharSequenceToString(argument); JavaTemplate template = JavaTemplate.builder("Reader.of(#{any(java.lang.CharSequence)})") .imports("java.io.Reader") - .contextSensitive() .build(); return template.apply(getCursor(), newClass.getCoordinates().replace(), argument); @@ -116,8 +116,7 @@ public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) { private Expression optimizeCharSequenceToString(Expression expr) { if (expr instanceof J.MethodInvocation) { J.MethodInvocation mi = (J.MethodInvocation) expr; - if ("toString".equals(mi.getSimpleName()) && - (mi.getArguments().isEmpty() || (mi.getArguments().size() == 1 && mi.getArguments().get(0) instanceof J.Empty)) && + if (TO_STRING_METHOD.matches(mi) && mi.getSelect() != null && TypeUtils.isAssignableTo("java.lang.CharSequence", mi.getSelect().getType())) { return mi.getSelect(); } From 7bda62760d92d95547cb3e8e11b1e720a0361a10 Mon Sep 17 00:00:00 2001 From: Johan Kragt Date: Wed, 10 Sep 2025 14:14:17 +0200 Subject: [PATCH 06/17] polish --- .../java/migrate/util/MigrateStringReaderToReaderOf.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java index f2513eb281..e1b275f175 100644 --- a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java +++ b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java @@ -102,13 +102,10 @@ private class TransformVisitor extends JavaVisitor { @Override public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) { if (STRING_READER_CONSTRUCTOR.matches(newClass)) { - Expression argument = newClass.getArguments().get(0); - argument = optimizeCharSequenceToString(argument); - JavaTemplate template = JavaTemplate.builder("Reader.of(#{any(java.lang.CharSequence)})") + return JavaTemplate.builder("Reader.of(#{any(java.lang.CharSequence)})") .imports("java.io.Reader") - .build(); - - return template.apply(getCursor(), newClass.getCoordinates().replace(), argument); + .build() + .apply(getCursor(), newClass.getCoordinates().replace(), optimizeCharSequenceToString(newClass.getArguments().get(0))); } return super.visitNewClass(newClass, ctx); } From 35ba5fa63374c3dd88f7a43a82ca8c44c82755ba Mon Sep 17 00:00:00 2001 From: Johan Kragt Date: Wed, 10 Sep 2025 14:28:11 +0200 Subject: [PATCH 07/17] Use java 25 for test --- .github/workflows/ci.yml | 4 ++++ build.gradle.kts | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d258e2d55e..43d89a4656 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,10 @@ concurrency: jobs: build: uses: openrewrite/gh-automation/.github/workflows/ci-gradle.yml@main + with: + java_version: | + 25-ea + 21 secrets: gradle_enterprise_access_key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} sonatype_username: ${{ secrets.SONATYPE_USERNAME }} diff --git a/build.gradle.kts b/build.gradle.kts index 3e39a436c7..f0d8caaa4c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -81,6 +81,12 @@ dependencies { testRuntimeOnly(gradleApi()) } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } +} + tasks.withType(Javadoc::class.java) { exclude("**/PlanJavaMigration.java") } From b7ea7f9271e5e0872d5165c24866f0b24860d955 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 10 Sep 2025 14:32:14 +0200 Subject: [PATCH 08/17] Use a parameterized test instead of repetition --- .../MigrateStringReaderToReaderOfTest.java | 134 ++++-------------- 1 file changed, 27 insertions(+), 107 deletions(-) diff --git a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java index a78959ab13..a758239e9a 100644 --- a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java +++ b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java @@ -16,11 +16,15 @@ 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 java.nio.CharBuffer; + import static org.openrewrite.java.Assertions.java; import static org.openrewrite.java.Assertions.javaVersion; @@ -126,116 +130,32 @@ StringReader createReader(String content) { ); } - @Test - void migrateWithStringBuilder() { - rewriteRun( - //language=java - java( - """ - import java.io.Reader; - import java.io.StringReader; - - class Test { - void test(StringBuilder sb) { - Reader reader = new StringReader(sb.toString()); - } - } - """, - """ - import java.io.Reader; - - class Test { - void test(StringBuilder sb) { - Reader reader = Reader.of(sb); - } - } - """ - ) - ); - } - - @Test - void migrateWithStringBuffer() { - rewriteRun( - //language=java - java( - """ - import java.io.Reader; - import java.io.StringReader; - - class Test { - void test(StringBuffer sb) { - Reader reader = new StringReader(sb.toString()); - } - } - """, - """ - import java.io.Reader; - - class Test { - void test(StringBuffer sb) { - Reader reader = Reader.of(sb); - } - } - """ - ) - ); - } - - @Test - void migrateWithCharBuffer() { + @ParameterizedTest + @ValueSource(strings = {"StringBuilder", "StringBuffer", "CharBuffer", "CharSequence"}) + void migrateCharSequenceVariants(String className) { + String extraImport = className.equals("CharBuffer") ? "\nimport java.nio.CharBuffer;" : ""; rewriteRun( //language=java java( - """ - import java.io.Reader; - import java.io.StringReader; - import java.nio.CharBuffer; - - class Test { - void test(CharBuffer cb) { - Reader reader = new StringReader(cb.toString()); - } - } - """, - """ - import java.io.Reader; - import java.nio.CharBuffer; - - class Test { - void test(CharBuffer cb) { - Reader reader = Reader.of(cb); - } - } - """ - ) - ); - } - - @Test - void migrateWithCharSequence() { - rewriteRun( - //language=java - java( - """ - import java.io.Reader; - import java.io.StringReader; - - class Test { - void test(CharSequence cs) { - Reader reader = new StringReader(cs.toString()); - } - } - """, - """ - import java.io.Reader; - - class Test { - void test(CharSequence cs) { - Reader reader = Reader.of(cs); - } - } - """ + String.format(""" + import java.io.Reader; + import java.io.StringReader;%s + + class Test { + void test(%s cs) { + Reader reader = new StringReader(cs.toString()); + } + } + """, extraImport, className), + String.format(""" + import java.io.Reader;%s + + class Test { + void test(%s cs) { + Reader reader = Reader.of(cs); + } + } + """, extraImport, className) ) ); } From 1d056de75595fd728a290052b9d7690c3d24586d Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 10 Sep 2025 14:32:35 +0200 Subject: [PATCH 09/17] Remove unused import --- .../java/migrate/util/MigrateStringReaderToReaderOfTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java index a758239e9a..eeb8d7f786 100644 --- a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java +++ b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java @@ -23,8 +23,6 @@ import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; -import java.nio.CharBuffer; - import static org.openrewrite.java.Assertions.java; import static org.openrewrite.java.Assertions.javaVersion; From 253356da5a4745d599adc735fcb31b223cc90f03 Mon Sep 17 00:00:00 2001 From: Johan Kragt Date: Wed, 10 Sep 2025 14:34:06 +0200 Subject: [PATCH 10/17] Updated java 25 for bot --- .github/workflows/receive-pr.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/receive-pr.yml b/.github/workflows/receive-pr.yml index cce051bb17..5d4edb1591 100644 --- a/.github/workflows/receive-pr.yml +++ b/.github/workflows/receive-pr.yml @@ -15,3 +15,7 @@ concurrency: jobs: upload-patch: uses: openrewrite/gh-automation/.github/workflows/receive-pr.yml@main + with: + java_version: | + 25-ea + 21 From aa6bfa231e43926dc4114efd2464a400727271ad Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 10 Sep 2025 14:36:40 +0200 Subject: [PATCH 11/17] Use method matcher in preconditions; use static class --- .../util/MigrateStringReaderToReaderOf.java | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java index e1b275f175..602ea572eb 100644 --- a/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java +++ b/src/main/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOf.java @@ -26,7 +26,7 @@ import org.openrewrite.java.JavaVisitor; import org.openrewrite.java.MethodMatcher; import org.openrewrite.java.search.UsesJavaVersion; -import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.search.UsesMethod; import org.openrewrite.java.tree.Expression; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaType; @@ -53,19 +53,15 @@ public String getDescription() { @Override public TreeVisitor getVisitor() { return Preconditions.check( - Preconditions.and( - new UsesJavaVersion<>(25), - new UsesType<>("java.io.StringReader", false) - ), + 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().getParent()); + return (J.VariableDeclarations.NamedVariable) new TransformVisitor().visitNonNull(v, ctx, getCursor().getParentOrThrow()); })); } return super.visitVariableDeclarations(mV, ctx); @@ -78,7 +74,7 @@ public J visitAssignment(J.Assignment a, ExecutionContext ctx) { if (TypeUtils.isOfClassType(variable.getType(), "java.io.Reader")) { maybeRemoveImport("java.io.StringReader"); maybeAddImport("java.io.Reader"); - return new TransformVisitor().visitNonNull(a, ctx, getCursor().getParent()); + return new TransformVisitor().visitNonNull(a, ctx, getCursor().getParentOrThrow()); } } return super.visitAssignment(a, ctx); @@ -92,36 +88,36 @@ public J visitReturn(J.Return r, ExecutionContext ctx) { if (TypeUtils.isOfClassType(returnType, "java.io.Reader")) { maybeRemoveImport("java.io.StringReader"); maybeAddImport("java.io.Reader"); - return new TransformVisitor().visitNonNull(r, ctx, getCursor().getParent()); + return new TransformVisitor().visitNonNull(r, ctx, getCursor().getParentOrThrow()); } } return super.visitReturn(r, ctx); } + } + ); + } - private 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 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; - } - } + 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; + } } } From a5a79cb7f8d828b2eb7102d258458f89d8b5d50d Mon Sep 17 00:00:00 2001 From: JohannisK Date: Wed, 10 Sep 2025 14:47:17 +0200 Subject: [PATCH 12/17] Update src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../java/migrate/util/MigrateStringReaderToReaderOfTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java index eeb8d7f786..c60697335d 100644 --- a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java +++ b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java @@ -131,8 +131,8 @@ StringReader createReader(String content) { @ParameterizedTest @ValueSource(strings = {"StringBuilder", "StringBuffer", "CharBuffer", "CharSequence"}) void migrateCharSequenceVariants(String className) { - String extraImport = className.equals("CharBuffer") ? "\nimport java.nio.CharBuffer;" : ""; - rewriteRun( + String extraImport = "CharBuffer".equals(className) ? "\nimport java.nio.CharBuffer;" : ""; + """ //language=java java( String.format(""" From 529d437eaa3f8ded04d9908b2c25f05ab7a538b2 Mon Sep 17 00:00:00 2001 From: JohannisK Date: Wed, 10 Sep 2025 14:47:34 +0200 Subject: [PATCH 13/17] Update src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../java/migrate/util/MigrateStringReaderToReaderOfTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java index c60697335d..e6c40c24e1 100644 --- a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java +++ b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java @@ -144,8 +144,8 @@ void test(%s cs) { Reader reader = new StringReader(cs.toString()); } } - """, extraImport, className), - String.format(""" + """.formatted( extraImport, className ), + """ import java.io.Reader;%s class Test { From f80499ab648ed00a1897e275cf6f84a856df34ef Mon Sep 17 00:00:00 2001 From: JohannisK Date: Wed, 10 Sep 2025 14:47:41 +0200 Subject: [PATCH 14/17] Update src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../java/migrate/util/MigrateStringReaderToReaderOfTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java index e6c40c24e1..e90d2413f1 100644 --- a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java +++ b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java @@ -153,7 +153,7 @@ void test(%s cs) { Reader reader = Reader.of(cs); } } - """, extraImport, className) + """.formatted( extraImport, className ) ) ); } From ba7f2c58dfa68969cb3a6e7aa77fffcd59c906c9 Mon Sep 17 00:00:00 2001 From: Johan Kragt Date: Wed, 10 Sep 2025 14:49:53 +0200 Subject: [PATCH 15/17] Fixed issue after bot commit --- .../java/migrate/util/MigrateStringReaderToReaderOfTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java index e90d2413f1..8ef105256c 100644 --- a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java +++ b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java @@ -132,7 +132,6 @@ StringReader createReader(String content) { @ValueSource(strings = {"StringBuilder", "StringBuffer", "CharBuffer", "CharSequence"}) void migrateCharSequenceVariants(String className) { String extraImport = "CharBuffer".equals(className) ? "\nimport java.nio.CharBuffer;" : ""; - """ //language=java java( String.format(""" From 5deee4a58657115afbe9c9f31356790e81eb0f67 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 10 Sep 2025 14:51:06 +0200 Subject: [PATCH 16/17] Apply formatter --- .../MigrateStringReaderToReaderOfTest.java | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java index 8ef105256c..edf981d886 100644 --- a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java +++ b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java @@ -31,8 +31,8 @@ class MigrateStringReaderToReaderOfTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { spec.recipe(new MigrateStringReaderToReaderOf()) - .parser(JavaParser.fromJavaVersion()) - .allSources(s -> s.markers(javaVersion(25))); + .parser(JavaParser.fromJavaVersion()) + .allSources(s -> s.markers(javaVersion(25))); } @DocumentExample @@ -132,28 +132,27 @@ StringReader createReader(String content) { @ValueSource(strings = {"StringBuilder", "StringBuffer", "CharBuffer", "CharSequence"}) void migrateCharSequenceVariants(String className) { String extraImport = "CharBuffer".equals(className) ? "\nimport java.nio.CharBuffer;" : ""; - //language=java - java( - String.format(""" - import java.io.Reader; - import java.io.StringReader;%s - - class Test { - void test(%s cs) { - Reader reader = new StringReader(cs.toString()); - } + //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), + """ + import java.io.Reader;%s + + class Test { + void test(%s cs) { + Reader reader = Reader.of(cs); } - """.formatted( extraImport, className ) - ) + } + """.formatted(extraImport, className) ); } From fc26686e40d2e1dec8ce6312ca683cd7e617a165 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 10 Sep 2025 15:17:46 +0200 Subject: [PATCH 17/17] Tolerate missing type information until we run on Java 25+ --- .github/workflows/ci.yml | 4 ---- .github/workflows/receive-pr.yml | 4 ---- build.gradle.kts | 6 ------ .../migrate/util/MigrateStringReaderToReaderOfTest.java | 2 ++ 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43d89a4656..d258e2d55e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,6 @@ concurrency: jobs: build: uses: openrewrite/gh-automation/.github/workflows/ci-gradle.yml@main - with: - java_version: | - 25-ea - 21 secrets: gradle_enterprise_access_key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} sonatype_username: ${{ secrets.SONATYPE_USERNAME }} diff --git a/.github/workflows/receive-pr.yml b/.github/workflows/receive-pr.yml index 5d4edb1591..cce051bb17 100644 --- a/.github/workflows/receive-pr.yml +++ b/.github/workflows/receive-pr.yml @@ -15,7 +15,3 @@ concurrency: jobs: upload-patch: uses: openrewrite/gh-automation/.github/workflows/receive-pr.yml@main - with: - java_version: | - 25-ea - 21 diff --git a/build.gradle.kts b/build.gradle.kts index f0d8caaa4c..3e39a436c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -81,12 +81,6 @@ dependencies { testRuntimeOnly(gradleApi()) } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(25)) - } -} - tasks.withType(Javadoc::class.java) { exclude("**/PlanJavaMigration.java") } diff --git a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java index edf981d886..6380eb05a0 100644 --- a/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java +++ b/src/test/java/org/openrewrite/java/migrate/util/MigrateStringReaderToReaderOfTest.java @@ -22,6 +22,7 @@ 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; @@ -31,6 +32,7 @@ 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))); }