Groovy: a trait with a static initializer block and a method is not print idempotent (ParseError)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 30/100
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
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 runparses 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.patchis a unified diff against
rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.javaat v8.90.4 (it applies with
git apply). The same project runs against the patched visitor:./gradlew runshows every variant parsing, printing
back exactly, and keeping its initializer blocks in the tree;./gradlew testrunsTraitInitializerTest, five tests
in theRewriteTeststyle forrewrite-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
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from openrewrite/rewrite
-
Difficulty 2/5 1-3 hours Newbie friendliness 79/100
openrewrite/rewrite#9835 ·
Maintainers usually reply within 1 day
-
bug gradle
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
openrewrite/rewrite#7576 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
openrewrite/rewrite#9834 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
openrewrite/rewrite#9819 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 4/5 3-5 days Newbie friendliness 55/100
openrewrite/rewrite#9817 ·
Maintainers usually reply within 1 day
All issues in openrewrite/rewrite
Similar issues
-
Four TestConstraintBasedAlgorithm tests pass when nothing is thrownPossibly taken @sharanggupta claimed this today. Open
Difficulty 1/5 1-3 hours Newbie friendliness 85/100
-
bug documentation good first issue help wanted priority low size XS
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
martin-francois/symphony-trello#1351 ·
Maintainers usually reply within 1 day
-
Add test coverage for ConvertUtil.parseDate() with NULL inputPossibly taken @mirajul-mollah7 claimed this today. Open
Difficulty 1/5 Under an hour Newbie friendliness 78/100
eclipse-sw360/sw360#4792 · 1 comment ·
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
aws/aws-emulator#30 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 4 days