Leave files unchanged when legacy Base64 calls cannot be fully migrated - #1195
Draft
martinfrancois wants to merge 2 commits into
Draft
Leave files unchanged when legacy Base64 calls cannot be fully migrated#1195martinfrancois wants to merge 2 commits into
martinfrancois wants to merge 2 commits into
Conversation
UseJavaUtilBase64 retyped BASE64Encoder and BASE64Decoder to Base64.Encoder and Base64.Decoder across the whole compilation unit while only rewriting the two overloads that have a java.util.Base64 equivalent. Every other legacy call, encode(InputStream, OutputStream) or decodeBuffer(String, OutputStream) for example, stayed on the new receiver type, so the recipe produced source that does not compile. Shapes it never retypes, such as a receiver declared as CharacterEncoder, a subclass of a legacy coder, or a method reference, left behind a sun.misc reference to a class removed in JDK 9. Scan the compilation unit first and return it untouched when a legacy coder type appears anywhere the recipe cannot retype or rewrite. The result is all or nothing: a compiling migration, or no change. The all-or-nothing scope is deliberate and costs some capability. A file that mixes one supported call with one unsupported call is no longer partially migrated, so cases that used to be rewritten in part are now left for a human. No existing test expectation changed; the new tests cover the full overload matrix and each untranslatable shape.
4 tasks
4 tasks
martinfrancois
marked this pull request as draft
August 16, 2026 01:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Suggested review order: 41 of 52 (Score: 2)
Review first: #1196
What's changed?
UseJavaUtilBase64now leaves a source file unchanged when it cannot migrate every use of the legacy coders in it. The legacy coders aresun.misc.BASE64Encoderandsun.misc.BASE64Decoder, which inherit their coder methods fromsun.misc.CharacterEncoderandsun.misc.CharacterDecoder.Current main uses
ChangeTypeto retype the two legacy coders tojava.util.Base64.Encoderandjava.util.Base64.Decoder. It rewrites only those calls for whichjava.util.Base64has an equivalent method, and leaves every other call as written, on a receiver whose type it has just changed.Before
Actual after the recipe
Using current main.
Expected after the recipe
(unchanged)The recipe makes no edit at all.
visitCompilationUnitkeeps its existing whole-file check,alreadyUsingIncompatibleBase64, and runs a second one,usesLegacyTypeUntranslatably, after it. When the new check reports true, the file is returned unchanged. It reports true when the file contains any of:BASE64EncoderorBASE64Decoderitself;CharacterEncoderorCharacterDecoder, whichChangeTypenever retypes;new BASE64Decoder() { };encoder::encode;CharacterEncoderorCharacterDecoder.A parameter declared as
BASE64EncoderorBASE64Decoderis none of those, sinceChangeTypedoes retype those two classes, so a file whose only legacy use is such a parameter is still migrated.What's your motivation?
Recipe:
org.openrewrite.java.migrate.UseJavaUtilBase64.The two legacy coders inherit 16 public coder methods from those supertypes, 10 from
CharacterEncoderand 6 fromCharacterDecoder. The recipe rewrites 3 of them, the same 3 on current main and on this branch:encode(byte[])andencodeBuffer(byte[])becomeBase64.getEncoder().encodeToString(byte[]), anddecodeBuffer(String)becomesBase64.getDecoder().decode(String). Calls to the remaining 13 are left as written, on a receiver current main has just retyped toBase64.EncoderorBase64.Decoder, neither of which declares them.The file current main produces therefore does not compile:
javac --release 11rejects the output block above withcannot find symbol: method encodeBuffer(byte[],OutputStream). The recipe still reports that file as successfully changed, so nothing in the run tells the user the result is broken.UseJavaUtilBase64runs insideJava8toJava11,UpgradeToJava17andUpgradeToJava21, so any of those three can turn a Java 8 codebase that compiles into one that does not. Reproduced on 3.41.0 and on 3.42.0-SNAPSHOT built from current main.Affected code in real projects
openjdk/jfxWebEngine.java: the Android port of the JavaFX WebView converts a user stylesheet URL to a data URL with the stream overloadnew sun.misc.BASE64Encoder().encodeBuffer(in, out); the recipe from main replaces the allocation withBase64.getEncoder()but cannot rewrite the two-argumentencodeBuffer, so the output callsBase64.getEncoder().encodeBuffer(in, out), a methodBase64.Encoderdoes not declare, and the file no longer compiles.Anything in particular you'd like reviewers to focus on?
No existing test expectation changed: the test file has added lines only, and the three tests already in
UseJavaUtilBase64Testare untouched. Three points:ChangeTyperetypes the whole file, so a file mixing a call the recipe can rewrite with one it cannot is now left entirely alone. The check is deliberately wide, so it also refuses some files current main migrates correctly, for example a supported method called on a receiver whose declared type is a type variable, as in<T extends BASE64Encoder> void write(T coder)callingcoder.encode(b), since a type variable is neither of the two receiver types the first bullet above accepts. I have not measured how often that shape occurs in real code.super.visitCompilationUnit(...). Checking afterwards and discarding the visit result would not work: rewritingdecodeBuffer(String)schedulesUnnecessaryCatchthroughdoAfterVisit, because thesun.miscdecodeBufferdeclaresthrows IOExceptionwhileBase64.Decoder.decodedoes not, and a visitor scheduled that way still runs even whenvisitCompilationUnitreturns the original tree.import java.util.Base64;when the file has no coder variable forChangeTypeto retype. The check does not visit imports, and nothing else in such a file is flagged, so it is still migrated, still without the import.J.Identifier, because it puts the original receiver back only for identifiers.factory().encode(b)is still rewritten toBase64.getEncoder().encodeToString(b)and the call tofactory()is lost. The check accepts it, becausefactory()is typedBASE64Encoder, whichChangeTypedoes retype.Fixing either one is a separate change.
Have you considered any alternatives or workarounds?
A skipped file is now skipped silently. One alternative is to report every skip with
Markup.warn, asvisitCompilationUnitalready does whenalreadyUsingIncompatibleBase64finds a class namedBase64that is notjava.util.Base64, with a message ending "Manual intervention required." That would tell users which files still need manual work, but it would also mark every skipped file in the diff with a/*~~(...)~~>*/comment, which is noisy on a codebase with manysun.misccall sites. The switch is one line invisitCompilationUnit, plus an updated expected value in the six new tests that expect their input file back unchanged. Tell me which you prefer and I will change it.Any additional context
This change adds 7 tests to
UseJavaUtilBase64Test, bringing that class to 10. Without the code change in this pull request, these 6 tests fail:unsupportedLegacyOverloadsLeaveTheCompilationUnitAloneoneUnsupportedOverloadSuppressesTheSupportedRewritesInTheSameFilemethodReferenceToLegacyCoderLeavesTheCompilationUnitAlonereceiverDeclaredAsLegacySupertypeLeavesTheCompilationUnitAlonesubclassOfLegacyEncoderLeavesTheCompilationUnitAloneanonymousSubclassOfLegacyDecoderLeavesTheCompilationUnitAloneThe seventh test,
stillMigratesHelperMethodWithLegacyEncoderParameter, passes either way. It covers a helper method whose parameter is declaredBASE64Encoder, which is still migrated.This change was prepared with AI assistance (Claude Code). I reviewed the code, the tests and this description.
I ran the formatter with the repository's
.editorconfig. It also wanted to re-indent lines that this change does not touch, so I left those alone and kept the diff limited to this change.Checklist
./gradlew buildlocally, and committed any resulting changes torecipes.csv