Lessons learnt developing Groovy AST transformations
Updated 17/01/2012: Info on handling exceptions
During the development of Grails 2.0 we shifted a significant amount of meta-programming logic to Groovy AST transformations.
The advantages of AST transformations are several. More flexible DSLs can be created, you don’t pay a runtime startup cost to use these DSLs and as a framework developer you can inline method calls where necessary.
Having said that Groovy AST development has its dark sides and below I’m going to list some lessons learnt developing AST transforms:
1. Avoid Global Transforms
Global AST transformations have their place, but they also have limitations. Their order is undetermined, you cannot have dependencies between transformations and they tend to be more difficult to debug.
We ended up rolling our own equivalent to global transforms using Spring class path scanning and other the Spring Ordered interface to control ordering.
2. Write Utility Methods
AST transformation code can be very repetitive. We ended up coming up with GrailsASTUtils as well as a series of base classes to make common transforms easy. Things like adding a method that delegates to property (like Groovy’s @Delegate) or the equivalent of Java reflection APIs for introspecting
ClassNode instances are available via
We also wrote an alternative to Groovy’s @Delegate called ApiDelegate that allows you to get a reference to the delegating instance and honors any annotations found in methods of the delegate.
3. Don’t Reuse AST nodes
Say you have a reference to a ClassNode instance obtained by inspecting the AST you’re transforming. Do NOT under any circumstances use this ClassNode to add a new method or property to the class being transformed. Doing stuff like:
MethodNode fooMethod = classNode.getMethod("foo", new Parameter) classNode.addMethod( new MethodNode("newMethod", Modifier.PUBLIC, fooMethod.getReturnType(), new Parameter, new ClassNode, methodBody); ) )
Where you are reusing the
ClassNode returned from
getReturnType() is very dangerous and lead to all sorts of generics related errors. Always create a new
ClassNode reference from an existing one using
MethodNode fooMethod = classNode.getMethod("foo", new Parameter) classNode.addMethod( new MethodNode("newMethod", Modifier.PUBLIC, ClassHelper.make(fooMethod.getReturnType().getName()), new Parameter, new ClassNode, methodBody); ) )
The above using of
ClassHelper.make(..) will save you loads of time, trust me.
4. Get comfortable with byte code and decompilers
Nobody said writing AST transformations was for the feint hearted and if you’re going to write any non-trivial transform you’ll need to get comfortable with using
javap and a Java decompiler just in case something goes wrong in the byte code generation phase.
I recommend JD-GUI for byte code decompilation and easy viewing. Whilst
javap is fine for seeing the raw byte code.
5. Never throw Exceptions from an AST transform
The Groovy compiler really doesn’t like it if you throw an exception from an AST transform. It also results in ugly errors to the user of your transform.
The proper way to report an error is via the
SourceUnit which has an
String messageText = "You can only negate a binary expressions in queries." Token token = Token.newString( expression.getText(), expression.getLineNumber(), expression.getColumnNumber()) LocatedMessage message = new LocatedMessage(messageText, token, sourceUnit) sourceUnit .getErrorCollector() .addError(message);
This will properly report a compilation error to the user. Notice too how you can pass information about the current expression node (line number, column number etc.) so that the compiler reports the location of the error nicely to the user.
Example AST Transforms in Grails
Finally, I thought I would list some of the AST transforms and what they do in Grails if you wish to see some examples in action:
- DetachedCriteriaTransformer - powers Grails 2.0’s new where queries.
- GormTransformer - wires in all of the GORM methods into each domain class’ byte code
- ControllerActionTransformer - transforms controller methods so that request parameters get auto-bound to method arguments
- ControllerTransformer - wires in the request, response, params etc. objects that form the controllers API
- GormToJpaTransform - Transforms a GORM entity to a JPA annotated entity at the byte code level. Part of the new GORM-JPA plugin.