From 1552cca790f479e62b0aa47c3169bbf2ec73c661 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sat, 4 Oct 2025 12:05:41 +0200 Subject: [PATCH 1/3] Fix UseVarForGenericMethodInvocations to handle nested generics with diamond operators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When converting to var, the recipe now makes nested generic type parameters explicit by replacing diamond operators (<>) with the actual type parameters from the variable declaration. This prevents compilation errors when using method references. Fixes #862 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../UseVarForGenericMethodInvocations.java | 73 ++++++++++++++--- ...UseVarForGenericMethodInvocationsTest.java | 80 +++++++++++++++++++ 2 files changed, 144 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java index 376c67d018..18cd86c6cc 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java @@ -15,15 +15,21 @@ */ package org.openrewrite.java.migrate.lang.var; +import org.jspecify.annotations.Nullable; import org.openrewrite.ExecutionContext; import org.openrewrite.Preconditions; import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.search.UsesJavaVersion; import org.openrewrite.java.tree.Expression; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeTree; + +import java.util.ArrayList; +import java.util.List; public class UseVarForGenericMethodInvocations extends Recipe { @Override @@ -82,16 +88,65 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations v maybeRemoveImport((JavaType.FullyQualified) vd.getType()); } - return DeclarationCheck.transformToVar(vd); - // TODO implement to support cases like `var strs = List.of();` - /*J.VariableDeclarations finalVd = vd; - return DeclarationCheck.transformToVar(vd, it -> { - // If left has generics but right has not, copy types parameters - if (finalVd.getTypeExpression() instanceof J.ParameterizedType && !((J.ParameterizedType) finalVd.getTypeExpression()).getTypeParameters().isEmpty() && it.getTypeParameters() == null) { - return it.withTypeParameters(((J.ParameterizedType) finalVd.getTypeExpression()).getPadding().getTypeParameters()); + // Make nested generic types explicit before converting to var + J.VariableDeclarations finalVd = vd; + return DeclarationCheck.transformToVar(vd, (J.MethodInvocation mi) -> makeNestedGenericsExplicit(mi, finalVd)); + } + + /** + * Makes nested generic types explicit by replacing diamond operators in constructor calls + * with explicit type parameters based on the variable declaration type. + */ + private J.MethodInvocation makeNestedGenericsExplicit(J.MethodInvocation mi, J.VariableDeclarations vd) { + // Extract type parameters from the variable declaration + if (!(vd.getTypeExpression() instanceof J.ParameterizedType)) { + return mi; + } + + J.ParameterizedType leftType = (J.ParameterizedType) vd.getTypeExpression(); + List leftTypeParams = leftType.getTypeParameters(); + if (leftTypeParams == null || leftTypeParams.isEmpty()) { + return mi; + } + + // Visit arguments and replace diamond operators with explicit type parameters + return mi.withArguments(ListUtils.map(mi.getArguments(), arg -> { + if (arg instanceof J.NewClass) { + J.NewClass newClass = (J.NewClass) arg; + List rightTypeParams = extractJavaTypes(newClass); + // Check if using diamond operator (rightTypeParams is empty) + if (rightTypeParams != null && rightTypeParams.isEmpty() && newClass.getClazz() instanceof J.ParameterizedType) { + // Copy type parameters from left side to right side + J.ParameterizedType rightType = (J.ParameterizedType) newClass.getClazz(); + return newClass.withClazz( + rightType.withTypeParameters(leftTypeParams) + ); + } } - return it; - });*/ + return arg; + })); + } + + /** + * Extract JavaTypes from a NewClass expression's type parameters. + * Returns null if not a parameterized type, or an empty list for diamond operator. + */ + private @Nullable List extractJavaTypes(J.NewClass newClass) { + TypeTree clazz = newClass.getClazz(); + if (clazz instanceof J.ParameterizedType) { + List typeParameters = ((J.ParameterizedType) clazz).getTypeParameters(); + List params = new ArrayList<>(); + if (typeParameters != null) { + for (Expression curType : typeParameters) { + JavaType type = curType.getType(); + if (type != null) { + params.add(type); + } + } + } + return params; + } + return null; } private static boolean allArgumentsEmpty(J.MethodInvocation invocation) { diff --git a/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocationsTest.java b/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocationsTest.java index 78d830b959..a2bc4568b4 100644 --- a/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocationsTest.java +++ b/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocationsTest.java @@ -259,5 +259,85 @@ void m() { ) ); } + + @Test + void nestedGenericsSimple() { + //language=java + rewriteRun( + version( + java(""" + package com.example.app; + + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; + + class A { + private void dostuff() { + List strs = Collections.synchronizedList(new ArrayList<>()); + } + } + """, """ + package com.example.app; + + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; + + class A { + private void dostuff() { + var strs = Collections.synchronizedList(new ArrayList()); + } + } + """), + 10 + ) + ); + } + + @Test + void nestedGenericsWithMethodReference() { + //language=java + rewriteRun( + version( + java(""" + package com.example.app; + + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; + + class A { + private void dostuff() { + List strs = Collections.synchronizedList(new ArrayList<>()); + strs.forEach(this::soString); + } + + private void soString(String s) { + System.out.println(s); + } + } + """, """ + package com.example.app; + + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; + + class A { + private void dostuff() { + var strs = Collections.synchronizedList(new ArrayList()); + strs.forEach(this::soString); + } + + private void soString(String s) { + System.out.println(s); + } + } + """), + 10 + ) + ); + } } } From 810f0a5fba229dba96db0839c715ab61ed460b92 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sat, 4 Oct 2025 12:30:33 +0200 Subject: [PATCH 2/3] Slight polish --- .../UseVarForGenericMethodInvocations.java | 7 +- ...UseVarForGenericMethodInvocationsTest.java | 92 +++++-------------- 2 files changed, 28 insertions(+), 71 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java index 18cd86c6cc..1bc351f135 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java @@ -42,7 +42,7 @@ public String getDisplayName() { public String getDescription() { //language=markdown return "Apply `var` to variables initialized by invocations of generic methods. " + - "This recipe ignores generic factory methods without parameters, because open rewrite cannot handle them correctly ATM."; + "This recipe ignores generic factory methods without parameters, because open rewrite cannot handle them correctly ATM."; } @Override @@ -119,7 +119,7 @@ private J.MethodInvocation makeNestedGenericsExplicit(J.MethodInvocation mi, J.V // Copy type parameters from left side to right side J.ParameterizedType rightType = (J.ParameterizedType) newClass.getClazz(); return newClass.withClazz( - rightType.withTypeParameters(leftTypeParams) + rightType.withTypeParameters(leftTypeParams) ); } } @@ -129,7 +129,8 @@ private J.MethodInvocation makeNestedGenericsExplicit(J.MethodInvocation mi, J.V /** * Extract JavaTypes from a NewClass expression's type parameters. - * Returns null if not a parameterized type, or an empty list for diamond operator. + * + * @return null if not a parameterized type, or an empty list for diamond operator. */ private @Nullable List extractJavaTypes(J.NewClass newClass) { TypeTree clazz = newClass.getClazz(); diff --git a/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocationsTest.java b/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocationsTest.java index a2bc4568b4..436fdce1b9 100644 --- a/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocationsTest.java +++ b/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocationsTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; +import org.openrewrite.Issue; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; @@ -260,81 +261,36 @@ void m() { ); } + @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/862") @Test void nestedGenericsSimple() { //language=java rewriteRun( version( - java(""" - package com.example.app; - - import java.util.ArrayList; - import java.util.Collections; - import java.util.List; - - class A { - private void dostuff() { - List strs = Collections.synchronizedList(new ArrayList<>()); - } - } - """, """ - package com.example.app; - - import java.util.ArrayList; - import java.util.Collections; - import java.util.List; - - class A { - private void dostuff() { - var strs = Collections.synchronizedList(new ArrayList()); - } - } - """), - 10 - ) - ); - } - - @Test - void nestedGenericsWithMethodReference() { - //language=java - rewriteRun( - version( - java(""" - package com.example.app; - - import java.util.ArrayList; - import java.util.Collections; - import java.util.List; - - class A { - private void dostuff() { - List strs = Collections.synchronizedList(new ArrayList<>()); - strs.forEach(this::soString); - } - - private void soString(String s) { - System.out.println(s); - } - } - """, """ - package com.example.app; - - import java.util.ArrayList; - import java.util.Collections; - import java.util.List; - - class A { - private void dostuff() { - var strs = Collections.synchronizedList(new ArrayList()); - strs.forEach(this::soString); + java( + """ + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; + + class A { + void dostuff() { + List strs = Collections.synchronizedList(new ArrayList<>()); + } } - - private void soString(String s) { - System.out.println(s); + """, + """ + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; + + class A { + void dostuff() { + var strs = Collections.synchronizedList(new ArrayList()); + } } - } - """), + """ + ), 10 ) ); From 84d6677be118e63d55393bd4e26ef7f1fd1634bf Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sat, 4 Oct 2025 17:34:22 +0200 Subject: [PATCH 3/3] No need to extract unused empty type parameters --- .../UseVarForGenericMethodInvocations.java | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java index 1bc351f135..4e352df206 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForGenericMethodInvocations.java @@ -28,9 +28,10 @@ import org.openrewrite.java.tree.JavaType; import org.openrewrite.java.tree.TypeTree; -import java.util.ArrayList; import java.util.List; +import static java.util.Objects.requireNonNull; + public class UseVarForGenericMethodInvocations extends Recipe { @Override public String getDisplayName() { @@ -103,8 +104,7 @@ private J.MethodInvocation makeNestedGenericsExplicit(J.MethodInvocation mi, J.V return mi; } - J.ParameterizedType leftType = (J.ParameterizedType) vd.getTypeExpression(); - List leftTypeParams = leftType.getTypeParameters(); + List leftTypeParams = ((J.ParameterizedType) vd.getTypeExpression()).getTypeParameters(); if (leftTypeParams == null || leftTypeParams.isEmpty()) { return mi; } @@ -113,41 +113,29 @@ private J.MethodInvocation makeNestedGenericsExplicit(J.MethodInvocation mi, J.V return mi.withArguments(ListUtils.map(mi.getArguments(), arg -> { if (arg instanceof J.NewClass) { J.NewClass newClass = (J.NewClass) arg; - List rightTypeParams = extractJavaTypes(newClass); // Check if using diamond operator (rightTypeParams is empty) - if (rightTypeParams != null && rightTypeParams.isEmpty() && newClass.getClazz() instanceof J.ParameterizedType) { + if (!hasTypeParams(newClass.getClazz())) { // Copy type parameters from left side to right side J.ParameterizedType rightType = (J.ParameterizedType) newClass.getClazz(); - return newClass.withClazz( - rightType.withTypeParameters(leftTypeParams) - ); + return newClass.withClazz(requireNonNull(rightType).withTypeParameters(leftTypeParams)); } } return arg; })); } - /** - * Extract JavaTypes from a NewClass expression's type parameters. - * - * @return null if not a parameterized type, or an empty list for diamond operator. - */ - private @Nullable List extractJavaTypes(J.NewClass newClass) { - TypeTree clazz = newClass.getClazz(); + private static boolean hasTypeParams(@Nullable TypeTree clazz) { if (clazz instanceof J.ParameterizedType) { List typeParameters = ((J.ParameterizedType) clazz).getTypeParameters(); - List params = new ArrayList<>(); if (typeParameters != null) { for (Expression curType : typeParameters) { - JavaType type = curType.getType(); - if (type != null) { - params.add(type); + if (curType.getType() != null) { + return true; } } } - return params; } - return null; + return false; } private static boolean allArgumentsEmpty(J.MethodInvocation invocation) {