This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch GROOVY_3_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/GROOVY_3_0_X by this push:
     new 88a8815a16 GROOVY-12144: Fix AstNodeToScriptAdapter decompiler 
rendering fidelity
88a8815a16 is described below

commit 88a8815a161f4ddb88f8cf8065c69a84ccbcf44e
Author: Leonard Brünings <[email protected]>
AuthorDate: Fri Jul 10 13:40:29 2026 +0200

    GROOVY-12144: Fix AstNodeToScriptAdapter decompiler rendering fidelity
    
    GROOVY_3_0_X backport of master commit 4b27025962. Applies the eight
    visitor fixes (nested generics, range bounds, elvis, attribute access,
    explicit closure arrow, safe index access, numeric literal suffixes,
    explicit method type arguments) to both copies of the class on this
    branch: groovy.console.ui.AstNodeToScriptAdapter (exercised by the test)
    and the legacy duplicate groovy.inspect.swingui.AstNodeToScriptAdapter.
    
    Adaptations for 3.0:
     - Range fix uses RangeExpression.isInclusive() and emits the `..<`
       right-exclusive form; 3.0 has no left-exclusive range syntax, so the
       two left-exclusive cases from master's range test are dropped.
     - Safe index access renders `?[`; 3.0 renders VariableExpressions with a
       trailing space, so output reads e.g. `list ?[0]` (verified to still
       re-parse and re-render as a safe index).
    
    Adds one regression test per defect (GroovyTestCase style). Reconciles
    existing tests to the branch's corrected output: testTernaryOperaters
    (elvis), testGenericsInMethods (nested generics now preserved),
    testLogAnnotation (@Log's Level.FINE AttributeExpression now renders
    .@FINE).
    
    Assisted-by: Claude Code (Opus 4.8)
---
 .../console/ui/AstNodeToScriptAdapter.groovy       | 32 +++++++--
 .../inspect/swingui/AstNodeToScriptAdapter.groovy  | 32 +++++++--
 .../console/ui/AstNodeToScriptAdapterTest.groovy   | 78 ++++++++++++++++++++--
 3 files changed, 130 insertions(+), 12 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 6cbda0e88a..7f2138ed32 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
@@ -391,6 +391,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
@@ -692,6 +695,7 @@ class AstNodeToScriptVisitor implements 
CompilationUnit.IPrimaryClassNodeOperati
             print '?'
         }
         print '.'
+        visitGenerics expression.genericsTypes
         Expression method = expression.method
         if (method instanceof ConstantExpression) {
             visitConstantExpression(method, true)
@@ -728,7 +732,11 @@ class AstNodeToScriptVisitor implements 
CompilationUnit.IPrimaryClassNodeOperati
     void visitBinaryExpression(BinaryExpression expression) {
         expression?.leftExpression?.visit this
         if (!(expression.rightExpression instanceof EmptyExpression) || 
expression.operation.type != Types.ASSIGN) {
-            print " $expression.operation.text "
+            if (expression?.operation?.text == '[') {
+                print expression.safe ? '?[' : ' [ '
+            } else {
+                print " $expression.operation.text "
+            }
             expression.rightExpression.visit this
 
             if (expression?.operation?.text == '[') {
@@ -759,6 +767,8 @@ class AstNodeToScriptVisitor implements 
CompilationUnit.IPrimaryClassNodeOperati
         if (expression?.parameters) {
             visitParameters(expression?.parameters)
             print ' ->'
+        } else if (expression?.parameters == null) {
+            print ' ->'
         }
         printLineBreak()
         indented {
@@ -792,7 +802,7 @@ class AstNodeToScriptVisitor implements 
CompilationUnit.IPrimaryClassNodeOperati
     void visitRangeExpression(RangeExpression expression) {
         print '('
         expression?.from?.visit this
-        print '..'
+        print expression.inclusive ? '..' : '..<'
         expression?.to?.visit this
         print ')'
     }
@@ -805,7 +815,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 {
@@ -830,6 +840,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'
+            }
         }
     }
 
@@ -1031,7 +1053,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/main/groovy/groovy/inspect/swingui/AstNodeToScriptAdapter.groovy
 
b/subprojects/groovy-console/src/main/groovy/groovy/inspect/swingui/AstNodeToScriptAdapter.groovy
index 41dc9b29ee..eb68b04677 100644
--- 
a/subprojects/groovy-console/src/main/groovy/groovy/inspect/swingui/AstNodeToScriptAdapter.groovy
+++ 
b/subprojects/groovy-console/src/main/groovy/groovy/inspect/swingui/AstNodeToScriptAdapter.groovy
@@ -400,6 +400,9 @@ class AstNodeToScriptVisitor extends 
PrimaryClassNodeOperation implements Groovy
                 }
                 first = false
                 print it.name
+                if (!it.placeholder && !it.wildcard) {
+                    visitGenerics it.type?.genericsTypes
+                }
                 if (it.upperBounds) {
                     print ' extends '
                     boolean innerFirst = true
@@ -701,6 +704,7 @@ class AstNodeToScriptVisitor extends 
PrimaryClassNodeOperation implements Groovy
             print '?'
         }
         print '.'
+        visitGenerics expression.genericsTypes
         Expression method = expression.method
         if (method instanceof ConstantExpression) {
             visitConstantExpression(method, true)
@@ -737,7 +741,11 @@ class AstNodeToScriptVisitor extends 
PrimaryClassNodeOperation implements Groovy
     void visitBinaryExpression(BinaryExpression expression) {
         expression?.leftExpression?.visit this
         if (!(expression.rightExpression instanceof EmptyExpression) || 
expression.operation.type != Types.ASSIGN) {
-            print " $expression.operation.text "
+            if (expression?.operation?.text == '[') {
+                print expression.safe ? '?[' : ' [ '
+            } else {
+                print " $expression.operation.text "
+            }
             expression.rightExpression.visit this
 
             if (expression?.operation?.text == '[') {
@@ -769,6 +777,8 @@ class AstNodeToScriptVisitor extends 
PrimaryClassNodeOperation implements Groovy
         if (expression?.parameters) {
             visitParameters(expression?.parameters)
             print ' ->'
+        } else if (expression?.parameters == null) {
+            print ' ->'
         }
         printLineBreak()
         indented {
@@ -802,7 +812,7 @@ class AstNodeToScriptVisitor extends 
PrimaryClassNodeOperation implements Groovy
     void visitRangeExpression(RangeExpression expression) {
         print '('
         expression?.from?.visit this
-        print '..'
+        print expression.inclusive ? '..' : '..<'
         expression?.to?.visit this
         print ')'
     }
@@ -815,7 +825,7 @@ class AstNodeToScriptVisitor extends 
PrimaryClassNodeOperation implements Groovy
         } else if (expression?.isSafe()) {
             print '?'
         }
-        print '.'
+        print expression instanceof AttributeExpression ? '.@' : '.'
         if (expression?.property instanceof ConstantExpression) {
             visitConstantExpression((ConstantExpression) expression?.property, 
true)
         } else {
@@ -840,6 +850,18 @@ class AstNodeToScriptVisitor extends 
PrimaryClassNodeOperation implements Groovy
             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'
+            }
         }
     }
 
@@ -1043,7 +1065,9 @@ class AstNodeToScriptVisitor extends 
PrimaryClassNodeOperation implements Groovy
 
     @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 41e0734881..eafb3ea5b1 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
@@ -159,8 +159,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') // GROOVY-12144: nested generics now preserved
+        assert result.contains('branches = new java.util.ArrayList<Tree<? 
extends java.lang.Object<V>>>()') // GROOVY-12144: nested generics now preserved
         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)')
@@ -322,7 +322,8 @@ 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')
+        // GROOVY-12144: @Log builds Level.FINE as an AttributeExpression, now 
rendered .@FINE
+        assert result.contains('return 
log.isLoggable(java.util.logging.Level.@FINE) ? log.fine(this.someMethod()) : 
null')
     }
 
     void testFieldDeclarationWithValue() {
@@ -709,7 +710,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() {
@@ -984,4 +985,73 @@ final class AstNodeToScriptAdapterTest extends 
GroovyTestCase {
         assert result =~ /(?s)oi:.*?\{.*?v \+= 2.*?\}/
     }
 
+    // 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  -- 3.0: only inclusive `1..5` and right-exclusive `1..<5` 
exist
+    void testRangeExpressionExclusiveBounds() {
+        String result = compileToScript('''def a = 1..5
+            def b = 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
+    // 3.0 renders VariableExpressions with a trailing space (e.g. `list `), 
so safe-index
+    // access reads `list ?[0]`; verified to still re-parse/re-render as a 
safe index.
+    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()')
+    }
+
 }

Reply via email to