This is an automated email from the ASF dual-hosted git repository.
daniellansun pushed a commit to branch GROOVY_4_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY_4_0_X by this push:
new 59683d2fcf GROOVY-12144: Fix AstNodeToScriptAdapter decompiler
rendering fidelity
59683d2fcf is described below
commit 59683d2fcf9da39427acbfaba6b6448a5e7bc410
Author: Leonard Brünings <[email protected]>
AuthorDate: Fri Jul 10 13:28:49 2026 +0200
GROOVY-12144: Fix AstNodeToScriptAdapter decompiler rendering fidelity
GROOVY_4_0_X backport of master commit 4b27025962. AstNodeToScriptVisitor
rendered several constructs incorrectly, producing source that did not
round-trip. Applies the eight independent visitor-method fixes and their
regression tests (nested generics, exclusive range bounds, Elvis, attribute
access, explicit zero-arg closure arrow, safe index access, numeric literal
suffixes, explicit method-call type arguments).
Assisted-by: Claude Code (Opus 4.8)
---
.../console/ui/AstNodeToScriptAdapter.groovy | 30 ++++++--
.../console/ui/AstNodeToScriptAdapterTest.groovy | 79 ++++++++++++++++++++--
2 files changed, 101 insertions(+), 8 deletions(-)
diff --git
a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
index dd5be0ed3c..384f790963 100644
---
a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
+++
b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
@@ -392,6 +392,9 @@ class AstNodeToScriptVisitor implements
CompilationUnit.IPrimaryClassNodeOperati
}
first = false
print it.name
+ if (!it.placeholder && !it.wildcard) {
+ visitGenerics it.type?.genericsTypes
+ }
if (it.upperBounds) {
print ' extends '
boolean innerFirst = true
@@ -689,6 +692,7 @@ class AstNodeToScriptVisitor implements
CompilationUnit.IPrimaryClassNodeOperati
print '?'
}
print '.'
+ visitGenerics expression.genericsTypes
Expression method = expression.method
if (method instanceof ConstantExpression) {
visitConstantExpression(method, true)
@@ -730,9 +734,9 @@ class AstNodeToScriptVisitor implements
CompilationUnit.IPrimaryClassNodeOperati
}
if (!(expression.rightExpression instanceof EmptyExpression) ||
expression.operation.type != Types.ASSIGN) {
String opText = expression?.operation?.text
- boolean isSubscriptOp = opText == '['
+ boolean isSubscriptOp = opText == '[' || opText == '?['
if (isSubscriptOp) {
- print "["
+ print expression.safe ? '?[' : opText
} else {
print " $opText "
}
@@ -773,6 +777,8 @@ class AstNodeToScriptVisitor implements
CompilationUnit.IPrimaryClassNodeOperati
if (expression?.parameters) {
visitParameters(expression?.parameters)
print ' ->'
+ } else if (expression?.parameters == null) {
+ print ' ->'
}
printLineBreak()
indented {
@@ -806,7 +812,9 @@ class AstNodeToScriptVisitor implements
CompilationUnit.IPrimaryClassNodeOperati
void visitRangeExpression(RangeExpression expression) {
print '('
expression?.from?.visit this
+ if (expression.exclusiveLeft) print '<'
print '..'
+ if (expression.exclusiveRight) print '<'
expression?.to?.visit this
print ')'
}
@@ -820,7 +828,7 @@ class AstNodeToScriptVisitor implements
CompilationUnit.IPrimaryClassNodeOperati
} else if (expression?.isSafe()) {
print '?'
}
- print '.'
+ print expression instanceof AttributeExpression ? '.@' : '.'
if (expression?.property instanceof ConstantExpression) {
visitConstantExpression((ConstantExpression) expression?.property,
true)
} else {
@@ -845,6 +853,18 @@ class AstNodeToScriptVisitor implements
CompilationUnit.IPrimaryClassNodeOperati
print "'$escaped'"
} else {
print expression.value
+ // re-append the literal's type suffix so re-parsing yields the
same type
+ // (Integer and BigDecimal are the literal defaults and need no
suffix)
+ def value = expression.value
+ if (value instanceof Long) {
+ print 'L'
+ } else if (value instanceof Float) {
+ print 'F'
+ } else if (value instanceof Double) {
+ print 'D'
+ } else if (value instanceof BigInteger) {
+ print 'G'
+ }
}
}
@@ -1071,7 +1091,9 @@ class AstNodeToScriptVisitor implements
CompilationUnit.IPrimaryClassNodeOperati
@Override
void visitShortTernaryExpression(ElvisOperatorExpression expression) {
- visitTernaryExpression(expression)
+ expression?.booleanExpression?.visit this
+ print ' ?: '
+ expression?.falseExpression?.visit this
}
@Override
diff --git
a/subprojects/groovy-console/src/test/groovy/groovy/console/ui/AstNodeToScriptAdapterTest.groovy
b/subprojects/groovy-console/src/test/groovy/groovy/console/ui/AstNodeToScriptAdapterTest.groovy
index be206753af..a977965b44 100644
---
a/subprojects/groovy-console/src/test/groovy/groovy/console/ui/AstNodeToScriptAdapterTest.groovy
+++
b/subprojects/groovy-console/src/test/groovy/groovy/console/ui/AstNodeToScriptAdapterTest.groovy
@@ -158,8 +158,8 @@ final class AstNodeToScriptAdapterTest extends
GroovyTestCase {
String result = compileToScript(script, CompilePhase.CLASS_GENERATION)
assert result.contains('public class Tree<V> extends java.lang.Object
implements groovy.lang.GroovyObject')
assert result.contains('private java.lang.Object<V> value') // todo:
is Object<V> correct? How do you know?
- assert result.contains('private java.util.List<Tree> branches') //
should the <? extends V> be dropped?
- assert result.contains('branches = new java.util.ArrayList<Tree>()')
// should the <? extends V> be dropped?
+ assert result.contains('private java.util.List<Tree<? extends
java.lang.Object<V>>> branches') // nested type args now rendered
+ assert result.contains('branches = new java.util.ArrayList<Tree<?
extends java.lang.Object<V>>>()') // nested type args now rendered
assert result.contains('public Tree(java.lang.Object<V> value)') //
again, is this correct?
assert result.contains(' public java.lang.Object<V> getValue()') // is
this correct?
assert result.contains('public void setValue(java.lang.Object<V>
value)')
@@ -344,7 +344,7 @@ final class AstNodeToScriptAdapterTest extends
GroovyTestCase {
String result = compileToScript(script, CompilePhase.CLASS_GENERATION)
assert result.contains('private static final transient
java.util.logging.Logger log')
assert result.contains("log =
java.util.logging.Logger.getLogger('Event')")
- assert result.contains('return
log.isLoggable(java.util.logging.Level.FINE) ? log.fine(this.someMethod()) :
null')
+ assert result.contains('return
log.isLoggable(java.util.logging.Level.@FINE) ? log.fine(this.someMethod()) :
null')
}
void testFieldDeclarationWithValue() {
@@ -738,7 +738,7 @@ final class AstNodeToScriptAdapterTest extends
GroovyTestCase {
foo ?: 'y'"""
String result = compileToScript(script, CompilePhase.SEMANTIC_ANALYSIS)
assert result.contains("true || false ? 'y' : 'n'")
- assert result.contains("foo ? foo : 'y'")
+ assert result.contains("foo ?: 'y'")
}
void testWhileLoop() {
@@ -1026,4 +1026,75 @@ final class AstNodeToScriptAdapterTest extends
GroovyTestCase {
String result = compileToScript(script, CompilePhase.SEMANTIC_ANALYSIS)
assert result.contains('(a as java.lang.String)')
}
+
+ // GROOVY-12144
+ void testNestedGenerics() {
+ String result = compileToScript('Map<String, List<Integer>> nested() {
null }')
+ assert result.contains('java.util.Map<String, List<Integer>> nested()')
+ }
+
+ // GROOVY-12144
+ void testRangeExpressionExclusiveBounds() {
+ String result = compileToScript('''def a = 1..5
+ def b = 1..<5
+ def c = 1<..5
+ def d = 1<..<5''')
+ assert result.contains('(1..5)')
+ assert result.contains('(1..<5)')
+ assert result.contains('(1<..5)')
+ assert result.contains('(1<..<5)')
+ }
+
+ // GROOVY-12144
+ void testElvisOperator() {
+ String result = compileToScript('def a = c ?: d')
+ assert result.contains('c ?: d')
+ assert !result.contains('c ? c : d')
+ }
+
+ // GROOVY-12144
+ void testAttributeExpression() {
+ String result = compileToScript('''def a = other.@order
+ def b = foos*.@order
+ def c = other?.@order''')
+ assert result.contains('other.@order')
+ assert result.contains('foos*.@order')
+ assert result.contains('other?.@order')
+ }
+
+ // GROOVY-12144
+ void testClosureParameterArrow() {
+ String result = compileToScript('''def implicitIt = { it * 2 }
+ def noArgs = { -> 'x' }''')
+ assert result.contains('{ ->')
+ assert result =~ /\{\s*it \* 2/
+ }
+
+ // GROOVY-12144
+ void testSafeIndexAccess() {
+ String result = compileToScript('''def a = list?[0]
+ def b = map?['key']
+ list?[1] = 42''')
+ assert result.contains('list?[0]')
+ assert result.contains("map?['key']")
+ assert result.contains('list?[1] = 42')
+ }
+
+ // GROOVY-12144
+ void testNumericLiteralSuffixes() {
+ String result = compileToScript('''def a = 42L
+ def b = 2.5f
+ def c = 3.5d
+ def d = 10G''')
+ assert result.contains('= 42L')
+ assert result.contains('= 2.5F')
+ assert result.contains('= 3.5D')
+ assert result.contains('= 10G')
+ }
+
+ // GROOVY-12144
+ void testExplicitMethodTypeArguments() {
+ String result = compileToScript('def a =
Collections.<String>emptyList()')
+ assert result.contains('java.util.Collections.<String>emptyList()')
+ }
}