diff --git a/.sdkmanrc b/.sdkmanrc index d2635abfaf..8e05285ec6 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=21.0.5-tem +java=21.0.7-tem diff --git a/src/main/java/org/openrewrite/java/migrate/lang/ReplaceUnusedVariablesWithUnderscore.java b/src/main/java/org/openrewrite/java/migrate/lang/ReplaceUnusedVariablesWithUnderscore.java new file mode 100644 index 0000000000..43680fef6a --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lang/ReplaceUnusedVariablesWithUnderscore.java @@ -0,0 +1,111 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.lang; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.RenameVariable; +import org.openrewrite.java.search.SemanticallyEqual; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.Statement; +import org.openrewrite.staticanalysis.VariableReferences; + +import java.util.concurrent.atomic.AtomicBoolean; + +@EqualsAndHashCode(callSuper = false) +@Value +public class ReplaceUnusedVariablesWithUnderscore extends Recipe { + + private static final String UNDERSCORE = "_"; + + @Override + public String getDisplayName() { + return "Replace unused variables with underscore"; + } + + @Override + public String getDescription() { + return "Replace unused variable declarations with underscore (_) for Java 22+. " + + "This includes unused variables in enhanced for loops, catch blocks, " + + "and lambda parameters where the variable is never referenced."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesJavaVersion<>(25), new JavaIsoVisitor() { + @Override + public J.ForEachLoop visitForEachLoop(J.ForEachLoop forLoop, ExecutionContext ctx) { + J.ForEachLoop l = super.visitForEachLoop(forLoop, ctx); + Statement variable = l.getControl().getVariable(); + if (variable instanceof J.VariableDeclarations) { + for (J.VariableDeclarations.NamedVariable namedVariable : ((J.VariableDeclarations) variable).getVariables()) { + renameVariableIfUnusedInContext(namedVariable, l.getBody()); + } + } + return l; + } + + @Override + public J.Try.Catch visitCatch(J.Try.Catch _catch, ExecutionContext ctx) { + J.Try.Catch c = super.visitCatch(_catch, ctx); + for (J.VariableDeclarations.NamedVariable namedVariable : c.getParameter().getTree().getVariables()) { + renameVariableIfUnusedInContext(namedVariable, c.getBody()); + } + return c; + } + + @Override + public J.Lambda visitLambda(J.Lambda lambda, ExecutionContext ctx) { + J.Lambda l = super.visitLambda(lambda, ctx); + for (J param : l.getParameters().getParameters()) { + if (param instanceof J.VariableDeclarations) { + for (J.VariableDeclarations.NamedVariable namedVariable : ((J.VariableDeclarations) param).getVariables()) { + renameVariableIfUnusedInContext(namedVariable, l.getBody()); + } + } + } + return l; + } + + private void renameVariableIfUnusedInContext(J.VariableDeclarations.NamedVariable variable, J context) { + if (!UNDERSCORE.equals(variable.getName().getSimpleName()) && + VariableReferences.findRhsReferences(context, variable.getName()).isEmpty() && + !usedInModifyingUnary(variable.getName(), context)) { + doAfterVisit(new RenameVariable<>(variable, UNDERSCORE)); + } + } + + private boolean usedInModifyingUnary(J.Identifier identifier, J context) { + return new JavaIsoVisitor() { + @Override + public J.Unary visitUnary(J.Unary unary, AtomicBoolean atomicBoolean) { + if (unary.getOperator().isModifying() && + SemanticallyEqual.areEqual(identifier, unary.getExpression())) { + atomicBoolean.set(true); + } + return super.visitUnary(unary, atomicBoolean); + } + }.reduce(context, new AtomicBoolean(false)).get(); + } + }); + } +} diff --git a/src/main/resources/META-INF/rewrite/java-version-25.yml b/src/main/resources/META-INF/rewrite/java-version-25.yml index 1bc043cf76..6b12e6bb39 100644 --- a/src/main/resources/META-INF/rewrite/java-version-25.yml +++ b/src/main/resources/META-INF/rewrite/java-version-25.yml @@ -28,6 +28,7 @@ recipeList: - org.openrewrite.java.migrate.UpgradeToJava21 - org.openrewrite.java.migrate.UpgradeBuildToJava25 - org.openrewrite.java.migrate.lang.MigrateProcessWaitForDuration + - org.openrewrite.java.migrate.lang.ReplaceUnusedVariablesWithUnderscore - org.openrewrite.java.migrate.util.MigrateInflaterDeflaterToClose - org.openrewrite.java.migrate.AccessController - org.openrewrite.java.migrate.RemoveSecurityPolicy diff --git a/src/test/java/org/openrewrite/java/migrate/lang/ReplaceUnusedVariablesWithUnderscoreTest.java b/src/test/java/org/openrewrite/java/migrate/lang/ReplaceUnusedVariablesWithUnderscoreTest.java new file mode 100644 index 0000000000..070802055c --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lang/ReplaceUnusedVariablesWithUnderscoreTest.java @@ -0,0 +1,443 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.lang; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.javaVersion; + +class ReplaceUnusedVariablesWithUnderscoreTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec + .recipe(new ReplaceUnusedVariablesWithUnderscore()) + .parser(JavaParser.fromJavaVersion()) + .allSources(s -> s.markers(javaVersion(25))); + } + + @DocumentExample + @Test + void replaceUnusedForEachVariable() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + + class Test { + int countOrders(List orders) { + int total = 0; + for (String order : orders) { + total++; + } + return total; + } + } + """, + """ + import java.util.List; + + class Test { + int countOrders(List orders) { + int total = 0; + for (String _ : orders) { + total++; + } + return total; + } + } + """ + ) + ); + } + + @Test + void doNotReplaceUsedForEachVariable() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + + class Test { + void processOrders(List orders) { + for (String order : orders) { + System.out.println(order); + } + } + } + """ + ) + ); + } + + @Test + void replaceUnusedCatchVariable() { + rewriteRun( + //language=java + java( + """ + class Test { + void parseNumber(String s) { + try { + Integer.parseInt(s); + } catch (NumberFormatException ex) { + System.out.println("Bad number: " + s); + } + } + } + """, + """ + class Test { + void parseNumber(String s) { + try { + Integer.parseInt(s); + } catch (NumberFormatException _) { + System.out.println("Bad number: " + s); + } + } + } + """ + ) + ); + } + + @Test + void doNotReplaceUsedCatchVariable() { + rewriteRun( + //language=java + java( + """ + class Test { + void parseNumber(String s) { + try { + Integer.parseInt(s); + } catch (NumberFormatException ex) { + System.out.println("Error: " + ex.getMessage()); + } + } + } + """ + ) + ); + } + + @Test + void replaceUnusedLambdaParameter() { + rewriteRun( + //language=java + java( + """ + import java.util.stream.Stream; + import java.util.stream.Collectors; + + class Test { + void example() { + Stream.of("a", "b", "c") + .collect(Collectors.toMap(String::toUpperCase, item -> "NODATA")); + } + } + """, + """ + import java.util.stream.Stream; + import java.util.stream.Collectors; + + class Test { + void example() { + Stream.of("a", "b", "c") + .collect(Collectors.toMap(String::toUpperCase, _ -> "NODATA")); + } + } + """ + ) + ); + } + + @Test + void doNotReplaceUsedLambdaParameter() { + rewriteRun( + //language=java + java( + """ + import java.util.stream.Stream; + import java.util.stream.Collectors; + + class Test { + void example() { + Stream.of("a", "b", "c") + .collect(Collectors.toMap(String::toUpperCase, item -> item.length())); + } + } + """ + ) + ); + } + + @Test + void noChangeOnJava21() { + rewriteRun( + spec -> spec.allSources(s -> s.markers(javaVersion(21))), + //language=java + java( + """ + import java.util.List; + + class Test { + int countOrders(List orders) { + int total = 0; + for (String order : orders) { + total++; + } + return total; + } + } + """ + ) + ); + } + + @Test + void handleNestedLoops() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + + class Test { + void example(List> matrix) { + for (List row : matrix) { + for (String item : row) { + System.out.println(); + } + } + } + } + """, + """ + import java.util.List; + + class Test { + void example(List> matrix) { + for (List row : matrix) { + for (String _ : row) { + System.out.println(); + } + } + } + } + """ + ) + ); + } + + @Test + void handleNestedLoopsBothUnused() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + + class Test { + void test(List> matrix) { + int count = 0; + for (List row : matrix) { + for (String item : matrix.get(0)) { + count++; + } + } + } + } + """, + """ + import java.util.List; + + class Test { + void test(List> matrix) { + int count = 0; + for (List _ : matrix) { + for (String _ : matrix.get(0)) { + count++; + } + } + } + } + """ + ) + ); + } + + @Test + void replaceBiConsumerBothParametersUnused() { + rewriteRun( + //language=java + java( + """ + import java.util.function.BiConsumer; + + class Test { + void test() { + BiConsumer consumer = (first, second) -> System.out.println(); + } + } + """, + """ + import java.util.function.BiConsumer; + + class Test { + void test() { + BiConsumer consumer = (_, _) -> System.out.println(); + } + } + """ + ) + ); + } + + @Test + void doNotReplaceUsedInMethodReference() { + rewriteRun( + //language=java + java( + """ + import java.util.stream.Stream; + + class Test { + void test() { + Stream.of("a").forEach(item -> System.out.println(item.toString())); + } + } + """ + ) + ); + } + + @Test + void replaceUnusedInNestedLambda() { + rewriteRun( + //language=java + java( + """ + import java.util.stream.Stream; + + class Test { + void test() { + Stream.of("a").forEach(outer -> { + Stream.of("b").forEach(inner -> System.out.println()); + }); + } + } + """, + """ + import java.util.stream.Stream; + + class Test { + void test() { + Stream.of("a").forEach(_ -> { + Stream.of("b").forEach(_ -> System.out.println()); + }); + } + } + """ + ) + ); + } + + @Test + void doNotReplaceWhenOuterVariableUsedInInnerLambda() { + rewriteRun( + //language=java + java( + """ + import java.util.stream.Stream; + + class Test { + void test() { + Stream.of("a").forEach(outer -> { + Stream.of("b").forEach(inner -> System.out.println(outer)); + }); + } + } + """, + """ + import java.util.stream.Stream; + + class Test { + void test() { + Stream.of("a").forEach(outer -> { + Stream.of("b").forEach(_ -> System.out.println(outer)); + }); + } + } + """ + ) + ); + } + + @ParameterizedTest + @ValueSource(strings = { + "++number; // Weird, but allowed", + "number++;", + }) + void doNotReplaceModifyingOperator(String body) { + rewriteRun( + //language=java + java( + """ + import java.util.List; + import java.util.stream.Stream; + + class Test { + int forloop(List numbers) { + for (int number : numbers) { + %s + } + } + } + """.formatted(body) + ) + ); + } + + @Test + void doNotReplaceUnrelatedVariables() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + + class Test { + int countOrders(List orders) { + for (String order : orders) { + String o = order; + } + } + } + """ + ) + ); + } +}