Skip to content

Do not run ExtractExplicitConstructorInvocationArguments on non-Java sources - #1235

Merged
knutwannheden merged 1 commit into
mainfrom
extractexplicitconstructorinvocationarguments-cce-on-groovy
Sep 8, 2026
Merged

Do not run ExtractExplicitConstructorInvocationArguments on non-Java sources#1235
knutwannheden merged 1 commit into
mainfrom
extractexplicitconstructorinvocationarguments-cce-on-groovy

Conversation

@knutwannheden

@knutwannheden knutwannheden commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Running ExtractExplicitConstructorInvocationArguments over a corpus of open-source repositories throws on spring-cloud/spring-cloud-contract, in spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy:

java.lang.ClassCastException: class org.openrewrite.java.tree.J$FieldAccess cannot be cast to class org.openrewrite.java.tree.J$MethodInvocation
  org.openrewrite.java.internal.template.JavaTemplateParser.parseMethodArguments(JavaTemplateParser.java:223)
  org.openrewrite.java.internal.template.JavaTemplateJavaExtension$1.visitMethodInvocation(JavaTemplateJavaExtension.java:420)
  org.openrewrite.java.JavaTemplate.apply(JavaTemplate.java:196)
  org.openrewrite.java.migrate.lang.ExtractExplicitConstructorInvocationArguments$1$1.visitMethodInvocation(ExtractExplicitConstructorInvocationArguments.java:150)

How a Groovy file gets here

The visitor is a JavaIsoVisitor, whose isAcceptable takes any JavaSourceFile, and that includes G.CompilationUnit. The UsesJavaVersion<>(25) precondition does not narrow it either: the JavaVersion marker on a Groovy source carries the module's Java version, so it matches.

Groovy attributes a method type to super(..) only under @CompileStatic, taken from StaticTypesMarker.DIRECT_METHOD_CALL_TARGET. SyntaxChecker.groovy annotates its nested classes that way, so the getMethodType() == null bail-out does not catch them and the recipe goes on to build a JavaTemplate.

Why the template stub comes back malformed

JavaTemplateParser.parseMethodArguments prints the call with its arguments stripped, then splices the new argument list in after the closing parenthesis:

String methodWithReplacementArgs = method.withArguments(emptyList()).printTrimmed(cursor.getParentOrThrow())
        .replaceAll("\\)$", template + (isStatement(cursor) ? ");" : ")"));

GroovyPrinter.visitMethodInvocation emits ( and ) only from inside its argument loop, so an empty argument list prints as a bare super, the replaceAll matches nothing, and the arguments are dropped without a word. That is the whole difference between the two stubs:

source stub handed to the Java parser
Groovy Object o = /*__TEMPLATE__*/super/*__TEMPLATE_STOP__*/;
Java Object o = /*__TEMPLATE__*/super(uri, kind);/*__TEMPLATE_STOP__*/;

A bare super parses as a J.FieldAccess, and the next line casts it to J.MethodInvocation. With a one-argument call the templated region is empty instead and the same line throws IndexOutOfBoundsException off .get(0).

The printer is doing something JavaPrinter does not — JavaPrinter delegates to visitContainer("(", …, ")"), which emits the delimiters whatever the container holds. It stays latent because a parsed Groovy LST never has an empty argument container: GroovyParserVisitor puts a J.Empty there for foo(). Only a synthesized withArguments(emptyList()) reaches it, which is exactly what parseMethodArguments does. Worth fixing in rewrite-groovy on its own merits, and tracked separately.

Fix

isAcceptable now returns sourceFile instanceof J.CompilationUnit. JEP 513 is a Java language feature and the transformation is written as a JavaTemplate, so Groovy and Kotlin are out of scope for this recipe whatever the LST happens to allow. Repairing the printer would let the template through, not make this recipe right on a Groovy source, so the guard is the fix here either way.

Nothing about the shape is specific to this recipe: any JavaTemplate-based recipe reaching parseMethodArguments without a source-file guard can hit the same line.

Tests

doNotRunOnGroovySources pins the guard with a minimal @CompileStatic Groovy source; without the fix it fails at JavaTemplateParser.java:223. The SimpleJavaFileObject subclass from SyntaxChecker.groovy reproduces the reported cast verbatim, but a two-line super(name.trim()) defends the same guard, so that is what the suite keeps.

Note for whoever reads CI: on JDK 25 this test class already fails 12 of 18 on main, which is #1206. This change adds no failures and its own test passes.

…a sources

The recipe's `JavaIsoVisitor` accepted any `JavaSourceFile`, so it also ran on
Groovy compilation units. There it reached `JavaTemplate`, which generates its
stub from the Groovy LST and parses it as Java, and
`JavaTemplateParser.parseMethodArguments` threw on the result.

Narrow the visitor to `J.CompilationUnit`. JEP 513 is a Java language feature,
so Groovy and Kotlin sources are out of scope for this recipe.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Sep 7, 2026
@knutwannheden
knutwannheden merged commit 572a24d into main Sep 8, 2026
1 check passed
@knutwannheden
knutwannheden deleted the extractexplicitconstructorinvocationarguments-cce-on-groovy branch September 8, 2026 01:49
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant