openrewrite / openrewrite/rewrite-migrate-java

ReplaceUnusedVariablesWithUnderscore renames existing unnamed catch parameter `_` to `Throwable_ _` when Java 25 sources are parsed on JDK 21

Open
#1,239 1 comment 0 reactions 0 assignees View on GitHub

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.rewrite 7.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 in visitVariables: 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.