Compile-Time Metaprogramming
Prerequisites: AST transformations, Runtime metaprogramming
AST transformations like @ToString run inside the compiler and
change the code before bytecode is generated. You can write your own —
it's how Spock, Micronaut, and many Groovy libraries work — and you can
configure the compiler to apply transformations automatically.
Compiler configuration
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.codehaus.groovy.control.customizers.ImportCustomizer
import groovy.transform.ToString
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
new ImportCustomizer().addStarImports('java.time'),
new ASTTransformationCustomizer(ToString, includeNames: true)
)
def shell = new GroovyShell(config)
println shell.evaluate('''
class Event { String name; LocalDate day }
new Event(name: 'launch', day: LocalDate.of(2026, 10, 1))
''')
Event(name:launch, day:2026-10-01)
The script being compiled never imported java.time or used
@ToString — the customizers added both. Gradle and Grails use
compiler configuration like this, and so can you: groovyc --configscript config.groovy applies a configuration to a whole
project (for example, @CompileStatic on every class).
Writing a local AST transformation
A transformation has two parts: an annotation that marks where it applies, and a transformation class that edits the AST.
import org.codehaus.groovy.ast.*
import org.codehaus.groovy.control.*
import org.codehaus.groovy.transform.*
import java.lang.annotation.*
import java.lang.reflect.Modifier
import static org.codehaus.groovy.ast.tools.GeneralUtils.*
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@GroovyASTTransformationClass('VersionedTransform')
@interface Versioned { String value() }
@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
class VersionedTransform extends AbstractASTTransformation {
void visit(ASTNode[] nodes, SourceUnit source) {
def annotation = nodes[0] as AnnotationNode
def target = nodes[1] as ClassNode
def version = getMemberStringValue(annotation, 'value')
target.addMethod('getVersion', Modifier.PUBLIC, ClassHelper.STRING_TYPE,
Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(constX(version)))
}
}
// The transformation must be compiled before the code that uses it,
// so this demo compiles the using code with a second GroovyShell:
def shell = new GroovyShell(this.class.classLoader)
println shell.evaluate('''
@Versioned('1.4.2')
class Api {}
new Api().version
''')
1.4.2
@GroovyASTTransformationClasslinks the annotation to the class that implements it.visitreceives the annotation and the annotated node, and can add methods, fields, or statements.GeneralUtilshas helpers such asreturnS(a return statement) andconstX(a constant expression).- The phase decides when it runs;
SEMANTIC_ANALYSISandCANONICALIZATIONare the usual choices.
In a real project the annotation and transformation live in their own
module (compiled first), and code in other modules just uses
@Versioned.
Macros
The groovy-macro module lets you write AST fragments as ordinary code
instead of building nodes by hand — macro { return "hello" } produces
the AST for that statement. It makes larger transformations far more
readable.
Inspecting the AST
groovyConsole has Script → Inspect AST (and groovyc has
-Dgroovy.ast=xml), which shows the tree for any code at any phase — the
best way to learn which nodes your transformation needs to produce.
Guidelines
- Reach for an existing transformation first —
groovy.transformcovers most needs. - Keep transformations small and well tested; compile errors from a buggy transformation are confusing for users.
- Report problems with
addError(message, node)so users see a normal compile error pointing at their code.