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

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


The following commit(s) were added to refs/heads/master by this push:
     new c9363dba83 GROOVY-12150: Show a closure's generated class in the AST 
browser decompiled view
c9363dba83 is described below

commit c9363dba83711e0b680546e275294a5ac1836485
Author: Paul King <[email protected]>
AuthorDate: Sun Jul 12 07:19:22 2026 +1000

    GROOVY-12150: Show a closure's generated class in the AST browser 
decompiled view
    
    Stamp each ClosureExpression, at closure-class creation, with the name of
    the synthetic class it compiles to (a new GENERATED_CLOSURE_CLASS node-
    metadata key on ClosureWriter), and have the console's 
AstNodeToScriptVisitor
    render that name as a trailing block comment on the closure/lambda literal.
    
    Previously the decompiled source showed the closure literal but never the
    `new <ClosureClass>(...)` construction the writer emits, so a closure's
    generated class was visible only in bytecode. The mark rides the existing
    class-creation path (one putNodeMetaData per closure, alongside the
    WriterControllerFactory one already there) and is only populated after the
    class-generation phase, so it is free for normal compilation and simply
    absent at earlier phases. The compiler itself never reads it.
---
 .../groovy/classgen/asm/ClosureWriter.java         |  9 ++++++++
 .../console/ui/AstNodeToScriptAdapter.groovy       | 15 +++++++++++++
 .../console/ui/AstNodeToScriptAdapterTest.groovy   | 26 ++++++++++++++++++++++
 3 files changed, 50 insertions(+)

diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java 
b/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java
index 982940f95c..6f89a136e8 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java
@@ -194,6 +194,14 @@ public class ClosureWriter {
         }
     }
 
+    /**
+     * Node metadata key set on a {@link ClosureExpression} once it has been 
compiled to a generated
+     * closure class, holding that class's name. Tooling such as the AST 
browser can read it to show
+     * which synthetic class a closure literal became; the compiler itself 
never consults it. The mark
+     * only becomes available after the class-generation phase, since that is 
when the class is made.
+     */
+    public static final String GENERATED_CLOSURE_CLASS = 
ClosureWriter.class.getName() + ".generatedClass";
+
     /**
      * Returns the existing generated class for a closure expression, or 
creates and registers
      * a new one if none exists yet.
@@ -209,6 +217,7 @@ public class ClosureWriter {
             closureClasses.put(expression, closureClass);
             controller.getAcg().addInnerClass(closureClass);
             closureClass.putNodeMetaData(WriterControllerFactory.class, 
(WriterControllerFactory) x -> controller);
+            expression.putNodeMetaData(GENERATED_CLOSURE_CLASS, 
closureClass.getName());
         }
         return closureClass;
     }
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 c86ccfb312..9a3f7e0257 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
@@ -94,6 +94,7 @@ import org.codehaus.groovy.ast.stmt.WhileStatement
 import org.codehaus.groovy.classgen.BytecodeExpression
 import org.codehaus.groovy.classgen.GeneratorContext
 import org.codehaus.groovy.classgen.Verifier
+import org.codehaus.groovy.classgen.asm.ClosureWriter
 import org.codehaus.groovy.control.CompilationFailedException
 import org.codehaus.groovy.control.CompilationUnit
 import org.codehaus.groovy.control.CompilePhase
@@ -852,6 +853,19 @@ class AstNodeToScriptVisitor implements 
CompilationUnit.IPrimaryClassNodeOperati
             expression?.code?.visit this
         }
         print '}'
+        printGeneratedClosureClass expression
+    }
+
+    /**
+     * Appends, as a trailing block comment, the name of the synthetic class a 
closure (or lambda)
+     * literal was compiled to. The mark is only present after the 
class-generation phase and only
+     * for literals that became a generated closure class, so nothing is 
printed otherwise.
+     */
+    private void printGeneratedClosureClass(ClosureExpression expression) {
+        def generatedClass = 
expression?.getNodeMetaData(ClosureWriter.GENERATED_CLOSURE_CLASS)
+        if (generatedClass) {
+            print " /* => ${generatedClass} */"
+        }
     }
 
     /** Renders a lambda expression. */
@@ -867,6 +881,7 @@ class AstNodeToScriptVisitor implements 
CompilationUnit.IPrimaryClassNodeOperati
             expression?.code?.visit this
         }
         print '}'
+        printGeneratedClosureClass expression
     }
 
     /** Renders a tuple expression. */
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 0de3b0a0bd..a975e7be26 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
@@ -1277,4 +1277,30 @@ final class AstNodeToScriptAdapterTest {
 
         assert result.contains('java.util.Collections.<String>emptyList()')
     }
+
+    @Test
+    void testClosureShowsGeneratedClassAtClassGeneration() {
+        String script = '''
+            class Demo {
+                def outer() { [1, 2].each { a -> [3, 4].each { b -> a + b } } }
+            }
+        '''
+        String result = compileToScript(script, CompilePhase.CLASS_GENERATION)
+
+        // each closure literal is annotated with the synthetic class it was 
compiled to,
+        // including the nesting of an inner closure within its enclosing 
closure class
+        assert result.contains('/* => Demo$_outer_closure1 */')
+        assert result.contains('/* => Demo$_outer_closure1$_closure2 */')
+    }
+
+    @Test
+    void testClosureGeneratedClassMarkAbsentBeforeClassGeneration() {
+        String script = '''
+            class Demo {
+                def outer() { [1, 2].each { it } }
+            }
+        '''
+        // the closure class does not exist yet, so nothing is rendered
+        assert !compileToScript(script, 
CompilePhase.SEMANTIC_ANALYSIS).contains('/* => ')
+    }
 }

Reply via email to