From 33325532e675fbdc0070c4edb6975e19826bd59c Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Thu, 28 May 2026 20:41:49 +0200 Subject: [PATCH] Retain load-bearing type witness in select position in `UnnecessaryExplicitTypeArguments` `UnnecessaryExplicitTypeArguments` removed an explicit type witness from a generic method invocation even when that invocation was the select (receiver) of another method invocation. In that position there is no target type to drive inference, so the witness is load-bearing and removing it produces non-compiling code (the type variable infers to `Object`). Split the enclosing-`J.MethodInvocation` handling into select vs. argument positions. In select position the witness is retained unless the type variables are inferable from the call's own arguments; the existing argument-position and static-method behavior is preserved by reusing the same inference check. --- .../UnnecessaryExplicitTypeArguments.java | 62 ++++++++++++------- .../UnnecessaryExplicitTypeArgumentsTest.java | 30 +++++++++ 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/openrewrite/staticanalysis/UnnecessaryExplicitTypeArguments.java b/src/main/java/org/openrewrite/staticanalysis/UnnecessaryExplicitTypeArguments.java index b0dd5cf8a..b66d08bef 100644 --- a/src/main/java/org/openrewrite/staticanalysis/UnnecessaryExplicitTypeArguments.java +++ b/src/main/java/org/openrewrite/staticanalysis/UnnecessaryExplicitTypeArguments.java @@ -53,24 +53,35 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Execu JavaType inferredType = null; if (enclosing instanceof J.MethodInvocation) { - if (shouldRetainOnStaticMethod(methodType)) { - return m; - } - // Cannot remove type parameters if it would introduce ambiguity about which method should be called J.MethodInvocation enclosingMethod = (J.MethodInvocation) enclosing; - if (enclosingMethod.getMethodType() == null) { - return m; - } - if (!(enclosingMethod.getMethodType().getDeclaringType() instanceof JavaType.Class)) { - return m; - } - JavaType.Class declaringClass = (JavaType.Class) enclosingMethod.getMethodType().getDeclaringType(); - // If there's another method on the class with the same name, skip removing type parameters - // More nuanced detection of ambiguity introduction is possible - if (declaringClass.getMethods().stream() - .filter(it -> it.getName().equals(enclosingMethod.getSimpleName())) - .count() > 1) { - return m; + if (enclosingMethod.getSelect() == method) { + // This invocation is the select (receiver) of the enclosing invocation, so the + // enclosing call provides no target type to drive inference of this call's type + // variables. Retain the witness unless those type variables can be inferred from + // this call's own arguments. + if (!canInferTypeArgumentsFromArguments(methodType)) { + return m; + } + } else { + // This invocation is an argument of the enclosing invocation. + if (shouldRetainOnStaticMethod(methodType)) { + return m; + } + // Cannot remove type parameters if it would introduce ambiguity about which method should be called + if (enclosingMethod.getMethodType() == null) { + return m; + } + if (!(enclosingMethod.getMethodType().getDeclaringType() instanceof JavaType.Class)) { + return m; + } + JavaType.Class declaringClass = (JavaType.Class) enclosingMethod.getMethodType().getDeclaringType(); + // If there's another method on the class with the same name, skip removing type parameters + // More nuanced detection of ambiguity introduction is possible + if (declaringClass.getMethods().stream() + .filter(it -> it.getName().equals(enclosingMethod.getSimpleName())) + .count() > 1) { + return m; + } } inferredType = methodType.getReturnType(); } else if (enclosing instanceof Expression) { @@ -143,13 +154,16 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Execu } private boolean shouldRetainOnStaticMethod(JavaType.Method methodType) { - if (!methodType.hasFlags(Flag.Static)) { - return false; - } - // Without arguments, the type parameter cannot be inferred from call-site parameters. - // Removing the explicit type arguments can break overload resolution in the enclosing call. + // Without a target type, removing the explicit type arguments of a static method can break + // overload resolution in the enclosing call when the type variables are not inferable from + // the call's own arguments. + return methodType.hasFlags(Flag.Static) && !canInferTypeArgumentsFromArguments(methodType); + } + + private boolean canInferTypeArgumentsFromArguments(JavaType.Method methodType) { + // Without arguments, the type parameters cannot be inferred from call-site arguments. if (methodType.getParameterTypes().isEmpty()) { - return true; + return false; } List formalTypeNames = new ArrayList<>(methodType.getDeclaredFormalTypeNames()); methodType.getParameterTypes().stream() @@ -157,7 +171,7 @@ private boolean shouldRetainOnStaticMethod(JavaType.Method methodType) { .flatMap(p -> ((JavaType.Parameterized) p).getTypeParameters().stream()) .filter(t -> t instanceof JavaType.GenericTypeVariable) .forEach(it -> formalTypeNames.remove(((JavaType.GenericTypeVariable) it).getName())); - return !formalTypeNames.isEmpty(); + return formalTypeNames.isEmpty(); } }); } diff --git a/src/test/java/org/openrewrite/staticanalysis/UnnecessaryExplicitTypeArgumentsTest.java b/src/test/java/org/openrewrite/staticanalysis/UnnecessaryExplicitTypeArgumentsTest.java index 593394320..19bd9cfd5 100644 --- a/src/test/java/org/openrewrite/staticanalysis/UnnecessaryExplicitTypeArgumentsTest.java +++ b/src/test/java/org/openrewrite/staticanalysis/UnnecessaryExplicitTypeArgumentsTest.java @@ -216,6 +216,36 @@ List test() { ); } + @Test + void retainsWitnessWhenResultIsSelectOfMethodInvocation() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + + class VarDec { + List getVariables() { + return null; + } + } + + class Cursor { + T getValue() { + return null; + } + } + + class Test { + String test(Cursor c) { + return c.getValue().getVariables().get(0); + } + } + """ + ) + ); + } + @Nested class StaticMethods { static final SourceSpecs GENERIC_CLASS_SOURCE = java(