diff --git a/src/main/java/org/openrewrite/java/migrate/UseJavaUtilBase64.java b/src/main/java/org/openrewrite/java/migrate/UseJavaUtilBase64.java index 8a75923253..f8e65a64a5 100644 --- a/src/main/java/org/openrewrite/java/migrate/UseJavaUtilBase64.java +++ b/src/main/java/org/openrewrite/java/migrate/UseJavaUtilBase64.java @@ -17,20 +17,25 @@ import com.fasterxml.jackson.annotation.JsonCreator; import lombok.Getter; +import org.jspecify.annotations.Nullable; import org.openrewrite.*; import org.openrewrite.java.ChangeType; +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.UsesType; import org.openrewrite.java.template.Semantics; +import org.openrewrite.java.tree.Expression; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaSourceFile; import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; import org.openrewrite.marker.Markup; import org.openrewrite.staticanalysis.UnnecessaryCatch; import java.util.Base64; +import java.util.concurrent.atomic.AtomicBoolean; public class UseJavaUtilBase64 extends Recipe { private final String sunPackage; @@ -65,6 +70,9 @@ public TreeVisitor getVisitor() { MethodMatcher base64EncodeMethod = new MethodMatcher(sunPackage + ".CharacterEncoder *(byte[])"); MethodMatcher base64DecodeBuffer = new MethodMatcher(sunPackage + ".CharacterDecoder decodeBuffer(String)"); + MethodMatcher anyEncoderMethod = new MethodMatcher(sunPackage + ".CharacterEncoder *(..)", true); + MethodMatcher anyDecoderMethod = new MethodMatcher(sunPackage + ".CharacterDecoder *(..)", true); + MethodMatcher newBase64Encoder = new MethodMatcher(sunPackage + ".BASE64Encoder ()"); MethodMatcher newBase64Decoder = new MethodMatcher(sunPackage + ".BASE64Decoder ()"); @@ -75,6 +83,11 @@ public J visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { return Markup.warn(cu, new IllegalStateException( "Already using a class named Base64 other than java.util.Base64. Manual intervention required.")); } + if (usesLegacyTypeUntranslatably(cu)) { + // Migrating only part of the file would either not compile or leave a `sun.misc` reference + // behind, so leave the whole file for a human + return cu; + } J.CompilationUnit c = (J.CompilationUnit) super.visitCompilationUnit(cu, ctx); c = (J.CompilationUnit) new ChangeType(sunPackage + ".BASE64Encoder", "java.util.Base64$Encoder", true) @@ -83,11 +96,123 @@ public J visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { .getVisitor().visitNonNull(c, ctx); } + /** + * True when a legacy coder type appears somewhere this recipe cannot translate: an expression statically + * typed as {@code CharacterEncoder} or {@code CharacterDecoder}, which {@link ChangeType} never retypes; a + * subclass or anonymous body, since {@code Base64.Encoder} has no accessible constructor; a call with no + * {@code java.util.Base64} equivalent, or on a receiver not typed as one of the two coders; a method + * reference, which is never rewritten; or a value passed to a parameter that stays a legacy coder. + */ + private boolean usesLegacyTypeUntranslatably(J.CompilationUnit cu) { + AtomicBoolean found = new AtomicBoolean(false); + new JavaIsoVisitor() { + @Override + public J.Import visitImport(J.Import anImport, AtomicBoolean found) { + // `ChangeType` rewrites imports, and no value flows through one + return anImport; + } + + @Override + public Expression visitExpression(Expression expression, AtomicBoolean found) { + if (isLegacySupertype(expression.getType())) { + found.set(true); + } + return super.visitExpression(expression, found); + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, AtomicBoolean found) { + if (classDecl.getExtends() != null && isLegacyCoderType(classDecl.getExtends().getType())) { + found.set(true); + } + return super.visitClassDeclaration(classDecl, found); + } + + @Override + public J.NewClass visitNewClass(J.NewClass newClass, AtomicBoolean found) { + // An anonymous creation's type is the anonymous class, so walk its supertypes too + if (newClass.getBody() != null && + (TypeUtils.isAssignableTo(sunPackage + ".CharacterEncoder", newClass.getType()) || + TypeUtils.isAssignableTo(sunPackage + ".CharacterDecoder", newClass.getType())) || + takesLegacySupertypeParameter(newClass.getMethodType())) { + found.set(true); + } + return super.visitNewClass(newClass, found); + } + + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, AtomicBoolean found) { + if (isLegacyCoderMethod(method.getMethodType())) { + Expression select = method.getSelect(); + if (!(encodeToString(method) || base64DecodeBuffer.matches(method)) || + select == null || !isRetypedCoderClass(select.getType())) { + found.set(true); + } + } else if (takesLegacySupertypeParameter(method.getMethodType())) { + found.set(true); + } + return super.visitMethodInvocation(method, found); + } + + @Override + public J.MemberReference visitMemberReference(J.MemberReference memberRef, AtomicBoolean found) { + JavaType.Method methodType = memberRef.getMethodType(); + if (isLegacyCoderMethod(methodType) || + methodType == null && isLegacyCoderType(memberRef.getContaining().getType()) || + methodType != null && (isRetypedCoderClass(methodType.getDeclaringType()) || + takesLegacySupertypeParameter(methodType))) { + found.set(true); + } + return super.visitMemberReference(memberRef, found); + } + }.visit(cu, found); + return found.get(); + } + + private boolean isLegacyCoderMethod(JavaType.@Nullable Method methodType) { + return methodType != null && (anyEncoderMethod.matches(methodType) || anyDecoderMethod.matches(methodType)); + } + + private boolean isRetypedCoderClass(@Nullable JavaType type) { + return TypeUtils.isOfClassType(type, sunPackage + ".BASE64Encoder") || + TypeUtils.isOfClassType(type, sunPackage + ".BASE64Decoder"); + } + + private boolean isLegacySupertype(@Nullable JavaType type) { + if (type instanceof JavaType.Array) { + return isLegacySupertype(((JavaType.Array) type).getElemType()); + } + return TypeUtils.isOfClassType(type, sunPackage + ".CharacterEncoder") || + TypeUtils.isOfClassType(type, sunPackage + ".CharacterDecoder"); + } + + private boolean isLegacyCoderType(@Nullable JavaType type) { + if (type instanceof JavaType.Array) { + return isLegacyCoderType(((JavaType.Array) type).getElemType()); + } + return isRetypedCoderClass(type) || isLegacySupertype(type); + } + + private boolean takesLegacySupertypeParameter(JavaType.@Nullable Method methodType) { + if (methodType != null) { + for (JavaType parameterType : methodType.getParameterTypes()) { + if (isLegacySupertype(parameterType)) { + return true; + } + } + } + return false; + } + + private boolean encodeToString(J.MethodInvocation method) { + return base64EncodeMethod.matches(method) && + ("encode".equals(method.getSimpleName()) || "encodeBuffer".equals(method.getSimpleName())); + } + @Override public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, ctx); - if (base64EncodeMethod.matches(m) && - ("encode".equals(method.getSimpleName()) || "encodeBuffer".equals(method.getSimpleName()))) { + if (encodeToString(m)) { m = JavaTemplate.builder(useMimeCoder ? "Base64.getMimeEncoder().encodeToString(#{anyArray(byte)})" : "Base64.getEncoder().encodeToString(#{anyArray(byte)})") .imports("java.util.Base64") .build() diff --git a/src/test/java/org/openrewrite/java/migrate/UseJavaUtilBase64Test.java b/src/test/java/org/openrewrite/java/migrate/UseJavaUtilBase64Test.java index d51c70eb3f..718bc74887 100644 --- a/src/test/java/org/openrewrite/java/migrate/UseJavaUtilBase64Test.java +++ b/src/test/java/org/openrewrite/java/migrate/UseJavaUtilBase64Test.java @@ -251,4 +251,195 @@ class Base64 { ) ); } + + @Test + void unsupportedLegacyOverloadsLeaveTheCompilationUnitAlone() { + rewriteRun( + //language=java + java( + """ + package test.sun.misc; + + import test.sun.misc.BASE64Decoder; + import test.sun.misc.BASE64Encoder; + import java.io.IOException; + import java.io.InputStream; + import java.io.OutputStream; + import java.nio.ByteBuffer; + + class Test { + void encode(InputStream stream, OutputStream output, byte[] bytes, ByteBuffer buffer) throws IOException { + BASE64Encoder encoder = new BASE64Encoder(); + encoder.encode(stream, output); + encoder.encode(bytes, output); + encoder.encode(buffer, output); + String encoded = encoder.encode(buffer); + encoder.encodeBuffer(stream, output); + encoder.encodeBuffer(bytes, output); + encoder.encodeBuffer(buffer, output); + encoded += encoder.encodeBuffer(buffer); + } + + void decode(InputStream stream, OutputStream output, String text) throws IOException { + BASE64Decoder decoder = new BASE64Decoder(); + decoder.decode(stream, output); + decoder.decode(text, output); + decoder.decodeBuffer(stream, output); + decoder.decodeBuffer(text, output); + byte[] first = decoder.decodeBuffer(stream); + } + } + """ + ) + ); + } + + @Test + void oneUnsupportedOverloadSuppressesTheSupportedRewritesInTheSameFile() { + rewriteRun( + //language=java + java( + """ + package test.sun.misc; + + import test.sun.misc.BASE64Encoder; + import java.io.IOException; + import java.io.OutputStream; + + class Test { + void test(byte[] bBytes, OutputStream output) throws IOException { + BASE64Encoder encoder = new BASE64Encoder(); + String encoded = encoder.encode(bBytes); + encoder.encodeBuffer(bBytes, output); + } + } + """ + ) + ); + } + + @Test + void methodReferenceToLegacyCoderLeavesTheCompilationUnitAlone() { + rewriteRun( + //language=java + java( + """ + package test.sun.misc; + + import test.sun.misc.BASE64Encoder; + import java.util.function.Function; + + class Test { + Function test() { + BASE64Encoder encoder = new BASE64Encoder(); + return encoder::encode; + } + } + """ + ) + ); + } + + @Test + void receiverDeclaredAsLegacySupertypeLeavesTheCompilationUnitAlone() { + rewriteRun( + //language=java + java( + """ + package test.sun.misc; + + import test.sun.misc.BASE64Decoder; + import test.sun.misc.BASE64Encoder; + import test.sun.misc.CharacterDecoder; + import test.sun.misc.CharacterEncoder; + import java.io.IOException; + + class Test { + void test(byte[] bBytes, String text) throws IOException { + CharacterEncoder encoder = new BASE64Encoder(); + String encoded = encoder.encode(bBytes); + encoded += encoder.encodeBuffer(bBytes); + CharacterDecoder decoder = new BASE64Decoder(); + byte[] decoded = decoder.decodeBuffer(text); + } + } + """ + ) + ); + } + + @Test + void subclassOfLegacyEncoderLeavesTheCompilationUnitAlone() { + rewriteRun( + //language=java + java( + """ + package test.sun.misc; + + import test.sun.misc.BASE64Encoder; + + class UrlSafeEncoder extends BASE64Encoder { + } + """ + ) + ); + } + + @Test + void anonymousSubclassOfLegacyDecoderLeavesTheCompilationUnitAlone() { + rewriteRun( + //language=java + java( + """ + package test.sun.misc; + + import test.sun.misc.BASE64Decoder; + + class Test { + BASE64Decoder decoder = new BASE64Decoder() { + }; + } + """ + ) + ); + } + + @Test + void stillMigratesHelperMethodWithLegacyEncoderParameter() { + rewriteRun( + //language=java + java( + """ + package test.sun.misc; + + import test.sun.misc.BASE64Encoder; + + class Test { + String test(byte[] bBytes) { + return encode(new BASE64Encoder(), bBytes); + } + + String encode(BASE64Encoder encoder, byte[] bBytes) { + return encoder.encode(bBytes); + } + } + """, + """ + package test.sun.misc; + + import java.util.Base64; + + class Test { + String test(byte[] bBytes) { + return encode(Base64.getEncoder(), bBytes); + } + + String encode(Base64.Encoder encoder, byte[] bBytes) { + return encoder.encodeToString(bBytes); + } + } + """ + ) + ); + } }