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('/* => ')
+ }
}