It's been baking for so long that I forgot Groovy 1.6 was still in release candidate mode! Well, it's here at last and Guillaume wrote a good overview on InfoQ.
Groovy 1.6 provides two options for hooking into the Groovy compiler for compile-time metaprogramming: local and global AST transformations. This post explains how to write and debug a local AST transformation.
An incredibly poor man's aspect oriented programming, if you will.
A local AST transformation is an easy way to do this. It requires two things: a definition of the @WithLogging annotation and an implementation of ASTTransformation that adds the logging expressions to the method.
An ASTTransformation is a callback that gives you access to the SourceUnit, through which you can get a reference to the AST. The AST is a tree structure of Expression objects that the source code has been transformed into. An easy way to learn about the AST is to explore it in a debugger, which will be shown shortly. Once you have the AST, you can analyze it to find out information about the code or rewrite it to add new functionality.
The annotation retention can be SOURCE, you won't need the annotation past that. The element type here is METHOD, the @WithLogging annotation applies to methods. But the most important part is the @GroovyASTTransformationClass annotation. This links the @WithLogging annotation to the ASTTransformation subclass you will write. gep.LoggingTransformation is the full package and class of my ASTTransformation. This line wires the annotation to the transformation.
With this in place, the Groovy compiler is going to try to invoke gep.LoggingASTTransformation every time an @WithLogging is found in a source unit. Any breakpoint set within LoggingASTTransformation will now be hit within the IDE when running the sample script.
The @GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) line tells the Groovy compiler that this is a local transformation that applies to the SEMANTIC_ANALYSIS CompilePhase. Local transformations can only be applied at semantic analysis or later phases, and this line is required!
The public visit(ASTNode, SourceUnit) method is invoked for each source unit that contains your target annotation. The AST you receive is not for the @WithLogging annotated method, it is for the entire file that contains @WithLogging. In this example, I'm just using findAll to locate methods that are annotated with @WithLogging, then using an each statement to wrap any annotated method with print lines. A method to the compiler is simply a list of Statement objects, so I'm adding a statement zero logging the start message and appending a statement to the list with the end message.
Note the creation of the new println statements in the createPrintlnAst(String) method. Creating AST for code is not simple. In this case I need to construct a new method call, passing in the receiver/variable, the name of the method, and an argument list. When creating AST I like to write the code I'm trying to create in a Groovy file and then inspect the AST of that code in the debugger to learn what I want to create. Then I write a function like createPrintlnAst using what I learned through the debugger. Cumbersome, for me at least.
An AST Builder is on the Groovy 1.7 roadmap. Search the Groovy-dev mailing list for "AST Builder" to contribute to the discussion. It'd be nice to hear what your proposed syntax is.
I'm also working on an AST Viewer within GroovyConsole for 1.7, which is almost done. Again, you can search groovy-dev for "AST Viewer" to see some screenshots and your feedback is valuable.
Great! I'm guessing you meant this discussion on the groovy-user list. I'll take a closer look. Thanks!
Thanks for this nice informative blog entry.