Groovy: a trait with a static initializer block and a method is not print idempotent (ParseError)

Open
#9,228 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
30/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
java
Domain
devtools

Research direction

Read rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java and the attached reproduction and proposed fix. The issue says the fix adds TraitInitializerTest under rewrite-groovy/src/test/java; run those tests and the reproduction to check that each trait variant parses, prints back exactly, and retains its initializer blocks.

Written by the indexing model from the issue text.

Description

bug groovy parser

What version of OpenRewrite are you using?

I am using

  • rewrite-groovy 8.90.4 (the latest release) and 8.90.3; both behave the same
  • rewrite-core and rewrite-java 8.90.4
  • Groovy 4.0.33 (as rewrite-groovy 8.90.4 resolves it); Java 17 and Java 25

How are you running OpenRewrite?

I am calling the parser directly from Java, with no build plugin. The reproduction (attached,
openrewrite-9228-reproduce.zip) is a single-module Gradle application depending only on rewrite-groovy:

plugins { id 'application' }
repositories { mavenCentral() }
dependencies {
    implementation 'org.openrewrite:rewrite-groovy:8.90.4'
    runtimeOnly 'org.slf4j:slf4j-nop:2.0.17'
}
java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
application { mainClass = 'example.Reproduce' }
SourceFile sf = GroovyParser.builder().build()
        .parse(new InMemoryExecutionContext(), source)
        .findFirst().orElseThrow();
// for the source below, sf is a ParseError

What is the smallest, simplest way to reproduce the problem?

trait T {
  static String X
  static {
    X = 'x'
  }
  def m() { 1 }
}

Each variant below was parsed with 8.90.3 and 8.90.4, with the same results:

Source Result
trait: static field + static block + method ParseError: not print idempotent, def m() {(){ 1 }
trait: static field + static block, no method parses and prints back, but the static block is missing from the tree: the class body holds only the field, and the block's text survives only as whitespace
trait: two static blocks between methods ParseError (GroovyParsingException)
trait: static block calling a static-imported method ParseError (GroovyParsingException)
trait: instance initializer block + method ParseError: not print idempotent, def m() {(){ y }
trait: static field + method, no static block parses
class: static field + static block + method parses; the class body holds the field, the static block and the method

So every trait with an initializer block, static or instance, is affected: the block is never visited, and the members
after it are parsed at the wrong position. With nothing after it, the block is silently dropped from the tree instead
of failing, which a recipe can't detect.

What did you expect to see?

A G.CompilationUnit that prints back exactly as the input:

trait T {
  static String X
  static {
    X = 'x'
  }
  def m() { 1 }
}

What did you see instead?

A ParseError. The tree prints the method's body mis-positioned after the static block:

trait T {
  static String X
  static {
    X = 'x'
  }
  def m() {(){ 1 }
}

What is the full stack trace of any errors you encountered?

With InMemoryExecutionContext, the ParseError carries:

java.lang.IllegalStateException: 681559262041625/file.groovy is not print idempotent.
diff --git a/681559262041625/file.groovy b/681559262041625/file.groovy
index c32445a..4aa5892 100644
--- a/681559262041625/file.groovy
+++ b/681559262041625/file.groovy
@@ -3,5 +3,5 @@
   static {
     X = 'x'
   }
-  def m() { 1 }
+  def m() {(){ 1 }
 }

  org.openrewrite.Parser.requirePrintEqualsInput(Parser.java:52)

In a larger real file (a Spock test helper: a trait with a static field, a static { } block that sets it, then helper
methods), the same shape fails earlier, on the first method call after the static block:

org.openrewrite.groovy.GroovyParsingException: Failed to parse ... at cursor position 998
Caused by: java.lang.IllegalArgumentException: Unable to parse method call
    at org.openrewrite.groovy.GroovyParserVisitor$RewriteGroovyVisitor.visitMethodCallExpression(...)
    at org.openrewrite.groovy.GroovyParserVisitor$RewriteGroovyVisitor.insideParentheses(...)

The failing expression there is "<name>:<secret>".bytes.encodeBase64().toString() inside a helper method. So the
cursor after the static initializer block appears to be off for the members that follow it, in traits only. Trait
support was added in #7499 and #7533; we found no existing issue about static initializers in traits.

Are you interested in contributing a fix to OpenRewrite?

Yes. We have a fix, attached with a reproduction, and would be glad to open a pull request:

  • openrewrite-9228-reproduce.zip: a minimal Gradle project (Maven Central, rewrite-groovy 8.90.4, Java 17
    toolchain). ./gradlew run parses each variant in the table above and prints whether it parses, whether it prints
    back exactly, and the class body's statements.
  • openrewrite-9228-fix.zip: the proposed fix. GroovyParserVisitor.patch is a unified diff against
    rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java at v8.90.4 (it applies with
    git apply). The same project runs against the patched visitor: ./gradlew run shows every variant parsing, printing
    back exactly, and keeping its initializer blocks in the tree; ./gradlew test runs TraitInitializerTest, five tests
    in the RewriteTest style for rewrite-groovy/src/test/java, which pass with the fix and fail without it.

The cause: Groovy's trait transformation moves a trait's initializer blocks into its synthetic $Trait$Helper class
(static blocks into $static$init$, instance blocks into $init$), where they keep their source positions, and
GroovyParserVisitor.visitClassBlock never collected them from there. Calls inside a static block are also rewritten
onto a synthetic $static$self receiver, which the visitor did not treat as synthetic the way it treats $self. The
fix collects the blocks from the helper and treats $static$self like $self; it changes nothing for classes,
interfaces, enums, records or scripts. The README in the fix project explains it in more detail.

If you'd prefer the pull request against main rather than v8.90.4, we can rebase it; guidance on anything you'd like
done differently is welcome.

Dominant language
Java
Stars
3.9k
Forks
599
Avg merge
9h 38m
Merged PRs (30d)
557

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from openrewrite/rewrite

All issues in openrewrite/rewrite

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.