diff --git a/src/main/java/org/openrewrite/java/migrate/lang/var/DeclarationCheck.java b/src/main/java/org/openrewrite/java/migrate/lang/var/DeclarationCheck.java index 6450259c39..24f616deba 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/var/DeclarationCheck.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/var/DeclarationCheck.java @@ -23,11 +23,14 @@ import org.openrewrite.java.tree.*; import org.openrewrite.marker.Markers; +import java.util.IdentityHashMap; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.UnaryOperator; import static java.util.Collections.emptyList; +import static java.util.Collections.newSetFromMap; import static java.util.Collections.singleton; import static java.util.Objects.requireNonNull; import static org.openrewrite.Tree.randomId; @@ -216,23 +219,108 @@ private static boolean isInsideInitializer(Cursor cursor, int nestedBlockLevel) * @return true iff is initialized by static method */ public static boolean initializedByStaticMethod(@Nullable Expression initializer) { - if (initializer == null) { + J.MethodInvocation invocation = getMethodInvocation(initializer); + if (invocation == null || invocation.getMethodType() == null) { return false; } + return invocation.getMethodType().hasFlags(Flag.Static); + } + + /** + * Checks whether the initializer {@linkplain Expression} is a {@linkplain J.MethodInvocation} of a generic method + * whose type parameter is only inferable from the declared type, as in {@code T getArgument(int index)}. + * + * @param initializer {@linkplain J.VariableDeclarations.NamedVariable#getInitializer()} value + * @return true iff is initialized by a generic method that needs the declared type to infer its return type + */ + public static boolean initializedByUnresolvableGenericMethod(@Nullable Expression initializer) { + J.MethodInvocation invocation = getMethodInvocation(initializer); + return invocation != null && isUnresolvableGenericMethod(invocation); + } + + private static J.@Nullable MethodInvocation getMethodInvocation(@Nullable Expression initializer) { + if (initializer == null) { + return null; + } initializer = initializer.unwrap(); if (!(initializer instanceof J.MethodInvocation)) { - // no MethodInvocation -> false + return null; + } + + return (J.MethodInvocation) initializer; + } + + private static boolean isUnresolvableGenericMethod(J.MethodInvocation mi) { + JavaType.Method mt = mi.getMethodType(); + if (mt == null || mi.getTypeParameters() != null) { + return false; + } + // The invocation's types are already resolved against the declared type, so the declaration is consulted instead + return declaresUninferableReturnType(mt.getDeclaringType(), mt.getName(), mt.getParameterTypes().size(), newIdentitySet()); + } + + private static boolean declaresUninferableReturnType(JavaType.@Nullable FullyQualified clazz, String name, int arity, Set seen) { + if (clazz == null || !seen.add(clazz)) { return false; } + for (JavaType.Method method : clazz.getMethods()) { + if (name.equals(method.getName()) && method.getParameterTypes().size() == arity && returnsUninferableTypeParameter(method)) { + return true; + } + } + if (declaresUninferableReturnType(clazz.getSupertype(), name, arity, seen)) { + return true; + } + for (JavaType.FullyQualified anInterface : clazz.getInterfaces()) { + if (declaresUninferableReturnType(anInterface, name, arity, seen)) { + return true; + } + } + return false; + } - J.MethodInvocation invocation = (J.MethodInvocation) initializer; - if (invocation.getMethodType() == null) { - // not a static method -> false + private static boolean returnsUninferableTypeParameter(JavaType.Method method) { + for (String typeParameterName : method.getDeclaredFormalTypeNames()) { + if (mentions(method.getReturnType(), typeParameterName, newIdentitySet()) && + !mentionsAny(method.getParameterTypes(), typeParameterName, newIdentitySet())) { + return true; + } + } + return false; + } + + private static boolean mentions(JavaType type, String typeParameterName, Set seen) { + if (!seen.add(type)) { return false; } + if (type instanceof JavaType.GenericTypeVariable) { + JavaType.GenericTypeVariable generic = (JavaType.GenericTypeVariable) type; + if (typeParameterName.equals(generic.getName())) { + return true; + } + return mentionsAny(generic.getBounds(), typeParameterName, seen); + } + if (type instanceof JavaType.Array) { + return mentions(((JavaType.Array) type).getElemType(), typeParameterName, seen); + } + if (type instanceof JavaType.Parameterized) { + return mentionsAny(((JavaType.Parameterized) type).getTypeParameters(), typeParameterName, seen); + } + return false; + } - return invocation.getMethodType().hasFlags(Flag.Static); + private static boolean mentionsAny(List types, String typeParameterName, Set seen) { + for (JavaType type : types) { + if (mentions(type, typeParameterName, seen)) { + return true; + } + } + return false; + } + + private static Set newIdentitySet() { + return newSetFromMap(new IdentityHashMap<>()); } /** 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 ef96dfc84c..5a38870a1d 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 @@ -58,10 +58,9 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations v } // Recipe specific - boolean isPrimitive = DeclarationCheck.isPrimitive(vd); - boolean usesNoGenerics = !DeclarationCheck.useGenerics(vd); - boolean usesTernary = DeclarationCheck.initializedByTernary(vd); - if (isPrimitive || usesTernary || usesNoGenerics) { + if (DeclarationCheck.isPrimitive(vd) || + DeclarationCheck.initializedByTernary(vd) || + !DeclarationCheck.useGenerics(vd)) { return vd; } @@ -73,9 +72,7 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations v J.MethodInvocation invocation = (J.MethodInvocation) originalInitializer.unwrap(); // If no type parameters and no arguments are present, we assume the type is too hard to determine - boolean hasNoTypeParams = invocation.getTypeParameters() == null; - boolean argumentsEmpty = allArgumentsEmpty(invocation); - if (hasNoTypeParams && argumentsEmpty) { + if (invocation.getTypeParameters() == null && allArgumentsEmpty(invocation)) { return vd; } diff --git a/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForObject.java b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForObject.java index 087dfe3501..4692183686 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForObject.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/var/UseVarForObject.java @@ -56,13 +56,13 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations v return vd; } - boolean isPrimitive = DeclarationCheck.isPrimitive(vd); - boolean usesGenerics = DeclarationCheck.useGenerics(vd); - boolean usesTernary = DeclarationCheck.initializedByTernary(vd); Expression initializer = vd.getVariables().get(0).getInitializer(); - boolean usesArrayInitializer = initializer instanceof J.NewArray; - boolean initializedByStaticMethod = DeclarationCheck.initializedByStaticMethod(initializer); - if (isPrimitive || usesGenerics || usesTernary || usesArrayInitializer || initializedByStaticMethod) { + if (DeclarationCheck.isPrimitive(vd) || + DeclarationCheck.useGenerics(vd) || + DeclarationCheck.initializedByTernary(vd) || + initializer instanceof J.NewArray || + DeclarationCheck.initializedByStaticMethod(initializer) || + DeclarationCheck.initializedByUnresolvableGenericMethod(initializer)) { return vd; } diff --git a/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForObjectsTest.java b/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForObjectsTest.java index 49d0f1d65b..92d0b42b44 100644 --- a/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForObjectsTest.java +++ b/src/test/java/org/openrewrite/java/migrate/lang/var/UseVarForObjectsTest.java @@ -232,6 +232,41 @@ void m() { ); } + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void typeToken() { + // `T` is inferred from the `Class` argument, not from the declared type + //language=java + rewriteRun( + version( + java(""" + package com.example.app; + + class A { + T typeToken(Class clazz) { + return null; + } + void m() { + String s = typeToken(String.class); + } + } + """, """ + package com.example.app; + + class A { + T typeToken(Class clazz) { + return null; + } + void m() { + var s = typeToken(String.class); + } + } + """), + 10 + ) + ); + } + @Test void subType() { //language=java @@ -317,7 +352,7 @@ void m() { @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/550") @Test - void genericType() { + void classLevelGenericType() { rewriteRun( //language=java java( @@ -344,12 +379,280 @@ void trigger() { ) ); } + + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void genericMethodWithTypeParameters() { + //language=java + rewriteRun( + java( + """ + package com.example.app; + + class A { + T method() { + return null; + } + void m() { + String strs = new A().method(); + } + } + """, + """ + package com.example.app; + + class A { + T method() { + return null; + } + void m() { + var strs = new A().method(); + } + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void genericMethodInferredFromArgument() { + //language=java + rewriteRun( + java( + """ + package com.example.app; + + class A { + T identity(T t) { + return t; + } + void m() { + String s = identity("x"); + } + } + """, + """ + package com.example.app; + + class A { + T identity(T t) { + return t; + } + void m() { + var s = identity("x"); + } + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void genericMethodInferredFromWildcardBoundedArgument() { + //language=java + rewriteRun( + java( + """ + package com.example.app; + + import java.util.function.Supplier; + + class A { + T orElse(Supplier supplier) { + return supplier.get(); + } + void m(Supplier supplier) { + String s = orElse(supplier); + } + } + """, + """ + package com.example.app; + + import java.util.function.Supplier; + + class A { + T orElse(Supplier supplier) { + return supplier.get(); + } + void m(Supplier supplier) { + var s = orElse(supplier); + } + } + """ + ) + ); + } } } @Nested class NotApplicable { + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void genericMethod() { + //language=java + rewriteRun( + java( + """ + package com.example.app; + + class A { + T method() { + return null; + } + void m() { + String s = method(); + } + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void genericMethodDeclaredOnInterface() { + //language=java + rewriteRun( + java( + """ + package com.example.app; + + interface Invocation { + T getArgument(int index); + } + class A { + void m(Invocation invocation) { + String s = invocation.getArgument(0); + } + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void genericMethodDeclaredOnGenericInterface() { + //language=java + rewriteRun( + java( + """ + package com.example.app; + + import java.util.List; + + interface Cache { + T get(K key); + T getAll(List keys); + } + class A { + void m(Cache cache, List keys) { + String s = cache.get("k"); + String t = cache.getAll(keys); + } + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void genericMethodInheritedFromSuperclass() { + //language=java + rewriteRun( + java( + """ + package com.example.app; + + class Base { + T get(int index) { + return null; + } + } + class Sub extends Base { + } + class A { + void m(Sub sub) { + String s = sub.get(0); + } + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void genericMethodReturningArray() { + //language=java + rewriteRun( + java( + """ + package com.example.app; + + class A { + S[] method() { + return null; + } + void m() { + String[] strs = method(); + } + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void genericMethodReturningMultiDimensionalArray() { + //language=java + rewriteRun( + java( + """ + package com.example.app; + + class A { + T[][] method() { + return null; + } + void m() { + String[][] strs = method(); + } + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite-migrate-java/pull/1187") + @Test + void genericMethodShadowingClassTypeParameter() { + //language=java + rewriteRun( + java( + """ + package com.example.app; + + class A { + T method() { + return null; + } + void m() { + String s = method(); + } + } + """ + ) + ); + } + @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/608") @Test void genericTypeInStaticMethod() {