Skip to content

Commit 8bd2c10

Browse files
JohannisKtimtebeekgithub-actions[bot]
authored
Java 25: Migrate new StringReader(String) to Reader.of(CharSequence) (#845)
* Setup * polish * Use `ctx` consistently * Use `ctx` in `visitAssignment` * polish * polish * Use java 25 for test * Use a parameterized test instead of repetition * Remove unused import * Updated java 25 for bot * Use method matcher in preconditions; use static class * 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> * 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> * 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> * Fixed issue after bot commit * Apply formatter * Tolerate missing type information until we run on Java 25+ --------- Co-authored-by: Tim te Beek <tim@moderne.io> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 5037eac commit 8bd2c10

File tree

3 files changed

+517
-0
lines changed

3 files changed

+517
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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.util;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.Preconditions;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.internal.ListUtils;
25+
import org.openrewrite.java.JavaTemplate;
26+
import org.openrewrite.java.JavaVisitor;
27+
import org.openrewrite.java.MethodMatcher;
28+
import org.openrewrite.java.search.UsesJavaVersion;
29+
import org.openrewrite.java.search.UsesMethod;
30+
import org.openrewrite.java.tree.Expression;
31+
import org.openrewrite.java.tree.J;
32+
import org.openrewrite.java.tree.JavaType;
33+
import org.openrewrite.java.tree.TypeUtils;
34+
35+
@EqualsAndHashCode(callSuper = false)
36+
@Value
37+
public class MigrateStringReaderToReaderOf extends Recipe {
38+
private static final MethodMatcher STRING_READER_CONSTRUCTOR = new MethodMatcher("java.io.StringReader <constructor>(java.lang.String)");
39+
private static final MethodMatcher TO_STRING_METHOD = new MethodMatcher("java.lang.Object toString()", true);
40+
41+
@Override
42+
public String getDisplayName() {
43+
return "Use `Reader.of(CharSequence)` for non-synchronized readers";
44+
}
45+
46+
@Override
47+
public String getDescription() {
48+
return "Migrate `new StringReader(String)` to `Reader.of(CharSequence)` in Java 25+. " +
49+
"This only applies when assigning to `Reader` variables or returning from methods that return `Reader`. " +
50+
"The new method creates non-synchronized readers which are more efficient when thread-safety is not required.";
51+
}
52+
53+
@Override
54+
public TreeVisitor<?, ExecutionContext> getVisitor() {
55+
return Preconditions.check(
56+
Preconditions.and(new UsesJavaVersion<>(25), new UsesMethod<>(STRING_READER_CONSTRUCTOR)),
57+
new JavaVisitor<ExecutionContext>() {
58+
@Override
59+
public J visitVariableDeclarations(J.VariableDeclarations mV, ExecutionContext ctx) {
60+
if (TypeUtils.isOfClassType(mV.getTypeAsFullyQualified(), "java.io.Reader")) {
61+
return mV.withVariables(ListUtils.map(mV.getVariables(), v -> {
62+
maybeRemoveImport("java.io.StringReader");
63+
maybeAddImport("java.io.Reader");
64+
return (J.VariableDeclarations.NamedVariable) new TransformVisitor().visitNonNull(v, ctx, getCursor().getParentOrThrow());
65+
}));
66+
}
67+
return super.visitVariableDeclarations(mV, ctx);
68+
}
69+
70+
@Override
71+
public J visitAssignment(J.Assignment a, ExecutionContext ctx) {
72+
if (a.getVariable() instanceof J.Identifier) {
73+
J.Identifier variable = (J.Identifier) a.getVariable();
74+
if (TypeUtils.isOfClassType(variable.getType(), "java.io.Reader")) {
75+
maybeRemoveImport("java.io.StringReader");
76+
maybeAddImport("java.io.Reader");
77+
return new TransformVisitor().visitNonNull(a, ctx, getCursor().getParentOrThrow());
78+
}
79+
}
80+
return super.visitAssignment(a, ctx);
81+
}
82+
83+
@Override
84+
public J visitReturn(J.Return r, ExecutionContext ctx) {
85+
J.MethodDeclaration method = getCursor().firstEnclosing(J.MethodDeclaration.class);
86+
if (method != null && method.getReturnTypeExpression() != null) {
87+
JavaType returnType = method.getReturnTypeExpression().getType();
88+
if (TypeUtils.isOfClassType(returnType, "java.io.Reader")) {
89+
maybeRemoveImport("java.io.StringReader");
90+
maybeAddImport("java.io.Reader");
91+
return new TransformVisitor().visitNonNull(r, ctx, getCursor().getParentOrThrow());
92+
}
93+
}
94+
return super.visitReturn(r, ctx);
95+
}
96+
}
97+
);
98+
}
99+
100+
private static class TransformVisitor extends JavaVisitor<ExecutionContext> {
101+
@Override
102+
public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) {
103+
if (STRING_READER_CONSTRUCTOR.matches(newClass)) {
104+
return JavaTemplate.builder("Reader.of(#{any(java.lang.CharSequence)})")
105+
.imports("java.io.Reader")
106+
.build()
107+
.apply(getCursor(), newClass.getCoordinates().replace(), optimizeCharSequenceToString(newClass.getArguments().get(0)));
108+
}
109+
return super.visitNewClass(newClass, ctx);
110+
}
111+
112+
private Expression optimizeCharSequenceToString(Expression expr) {
113+
if (expr instanceof J.MethodInvocation) {
114+
J.MethodInvocation mi = (J.MethodInvocation) expr;
115+
if (TO_STRING_METHOD.matches(mi) &&
116+
mi.getSelect() != null && TypeUtils.isAssignableTo("java.lang.CharSequence", mi.getSelect().getType())) {
117+
return mi.getSelect();
118+
}
119+
}
120+
return expr;
121+
}
122+
}
123+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ recipeList:
3131
- org.openrewrite.java.migrate.lang.MigrateProcessWaitForDuration
3232
- org.openrewrite.java.migrate.lang.ReplaceUnusedVariablesWithUnderscore
3333
- org.openrewrite.java.migrate.util.MigrateInflaterDeflaterToClose
34+
- org.openrewrite.java.migrate.util.MigrateStringReaderToReaderOf
3435
- org.openrewrite.java.migrate.AccessController
3536
- org.openrewrite.java.migrate.RemoveSecurityPolicy
3637
- org.openrewrite.java.migrate.RemoveSecurityManager

0 commit comments

Comments
 (0)