openrewrite / openrewrite/rewrite-migrate-java
ReplaceUnusedVariablesWithUnderscore renames existing unnamed catch parameter `_` to `Throwable_ _` when Java 25 sources are parsed on JDK 21
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 156
- Forks
- 130
- Avg merge
- 20h 57m
- Merged PRs (30d)
- 21
Description
Follow-up to https://github.com/openrewrite/rewrite-migrate-java/pull/844
🤖 Generated with Claude Code
What version of OpenRewrite are you using?
- rewrite-recipe-bom 3.37.0 → rewrite-migrate-java 3.42.0, rewrite-java(-25) 8.89.0
- Gradle plugin
org.openrewrite.rewrite7.39.0 - Sources compiled with Java 25
What is the smallest, simplest way to reproduce the problem?
Reduced from jabgui/src/main/java/org/jabref/Launcher.java in JabRef:
class A {
void m() {
try {
System.out.println();
} catch (Throwable _) {
}
}
}
Run org.openrewrite.java.migrate.lang.ReplaceUnusedVariablesWithUnderscore.
What did you expect to see?
No change: the catch parameter is already _.
What did you see instead?
} catch (Throwable_ _) {
Uncompilable. It happens on every run, so the recipe is not idempotent: in JabRef, 175 catch clauses already using _ were changed, and after reverting them a second run changed them again (lambda parameters _ are left alone). The UNDERSCORE.equals(variable.getName().getSimpleName()) guard apparently does not match unnamed catch parameters coming from the Java 25 parser.
Root cause (added after investigation)
Not the recipe: the Gradle daemon ran on JDK 21 while the sources are Java 25. The OpenRewrite Gradle plugin parses with the JVM running Gradle, not with the project's toolchain, so ReloadableJava21ParserVisitor handled the Java 25 sources. Its visitVariables has no handling for unnamed variables: javac hands over an empty name, sourceBefore("") consumes nothing, and _ ends up in the whitespace before ). The recipe then sees a variable named "", "renames" it, and Throwable + _ + _ is printed. Enhanced-for and lambda parameters named _ become J.Erroneous on the Java 21 parser and are therefore skipped, which is why only catch clauses were affected.
On JDK 25 the Java 25 parser maps the empty name to _ (ReloadableJava25ParserVisitor.visitVariables) and the recipe leaves those variables alone; two consecutive runs on JabRef produce no further changes.
Possible hardening, either or both:
rewrite-java-21: mirror the Java 25 visitor invisitVariables:String varName = n.getName().isEmpty() ? "_" : n.getName().toString();so the LST prints idempotently.- Recipe: do not rename a variable whose simple name is empty (
renameVariableIfUnusedInContext).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with ReloadableJava21ParserVisitor.visitVariables and the recipe entry point renameVariableIfUnusedInContext, comparing the Java 21 and Java 25 visitor behavior described in the issue. Reproduce the catch-parameter case on JDK 21, then verify the chosen hardening keeps the parameter unchanged and that repeated recipe runs remain idempotent.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- compilers, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100