Thursday, March 26, 2009

Groovy 1.6 AST Transformation Example

With Groovy 1.6 released and this article on InfoQ got me to try some new features.

AST Transformation
With Groovy 1.6 you can define local and global transformation using annotations.
So let's define @AssertParamsNotNull annotation for method which would perform AST Transformation at compile time. It simply asserts parameters of method are not null. I am following groovy documentation here and blog entry here by Hamlet D'Arcy.

There are three components to defining local AST Transformation

Step 1: Define Annotation
Step 2: Define GroovyASTTransformation
Step 3: Test and Usage

Pre-requisites
Download Groovy 1.6
Download Groovy Eclipse plugin for 1.6, available from this update URL.
 http://dist.groovy.codehaus.org/distributions/updateDev_1.6/
This url is different from their usual update url. I believe they will update their distributions update url (http://dist.codehaus.org/groovy/distributions/update/)

Step 1: Define Annotation @AssertParamsNotNull

package com.learn.groovy16.ast.local
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
import java.lang.annotation.ElementType
import org.codehaus.groovy.transform.GroovyASTTransformationClass
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["com.learn.groovy16.ast.local.AssertParamsNotNullASTTransformation"])
public @interface AssertParamsNotNull{
One thing to notice is here defining the ASTTransformation class. It requires complete qualified class name. Rest of the things are standard annotation declaration.

While working on getting this running in my environment I faced following errors/issues.

Unknown Type: ANNOTATION_DEF at line ...
This error comes if for some reason eclipse environment is still using older groovy installation. Check your GROOVY_HOME points to Groovy 1.6. If you are able to run this through command line, but eclipse is giving your errors. Update your Groovy Eclipse plugin from dev update URL. You may have to wipe it clean for eclipse to understand it.

Expected '{' but was found...
This error comes if for some reason there funny newline characters. I think I got this error if right after "public @interface AssertParamsNotNull" if { is in the next line. I believe I got funny character in between those two. I changed the text file encoding to UTF-8 and the error went away. It allowed me to have braces on the next line.

Step 2: Define GroovyASTTransformation


package com.learn.groovy16.ast.local
import org.codehaus.groovy.transform.GroovyASTTransformation
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.Parameter
import org.codehaus.groovy.ast.stmt.Statement
import org.codehaus.groovy.ast.stmt.AssertStatement
import org.codehaus.groovy.ast.expr.BooleanExpression
import org.codehaus.groovy.ast.expr.NotExpression
import org.codehaus.groovy.ast.expr.VariableExpression
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
public class AssertParamsNotNullASTTransformation implements ASTTransformation
{
public void visit(ASTNode[] nodes, SourceUnit source)
{
List methods = source.getAST()?.getMethods()
methods.findAll{MethodNode method ->
method.getAnnotations(new ClassNode(AssertParamsNotNull))
}.each{MethodNode method ->
List existingStatements = method.getCode().getStatements()
Parameter[] parameters = method.getParameters()
parameters.eachWithIndex(){ parameter, i ->
existingStatements.add(i, createAssertStatement(parameter))
}
}
}

public Statement createAssertStatement(Parameter parameter){
return new AssertStatement(
new BooleanExpression(
new VariableExpression(parameter))
}
}

This is where most of the trick is happening. This class implements ASTTransformation interface implementing visit(ASTNode[], SourceUnit source) method. This method gets all methods marked with AssertParamsNotNull annotation, iterates over all method, iterates over all method parameters and calls createAssertStatement(parameter). createAssertStatement(parameter) creates Statements equivalent to assert parameter and inserts it into the AST tree. All this happens at compile time. So notice line
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
Creating Statement and Expression using API is really cumbersome. It is good they are working on builder support for the same.
 
Step 3: Test and Run


package com.learn.groovy16.ast.local

@com.learn.groovy16.ast.local.AssertParamsNotNull

def foo(String var)
{
println var
}
foo("Hello")
foo(null)


Note the annotation with fully qualified name.

Interesting thing is that all transformation happens at compile time. It does not stop

Other interesting AST Transformations...

@CatchExceptions(list=[exception1, exception2, ..],rethrow=AppException)
Define AST transformation to remove clutterred exception handling logic and rethrow ApplicationException as noted by parameters of annotation. I think the challenge here will be to build AST Statement and Expression.

@SecureAccess(Role=Manager)
Allows to define security over method execution.

@Trace(isTraceEnabled=$GlobalTraceParameter)
Emits out trace of method. Entered, parameter values, exit, return type. This only gets emitted if GlobalTraceParameter is set to Y

6 comments:

Unknown said...

What exception is thrown from step #3 at line foo(null), and can you explicitly specify the exception implementation?

Hamlet D'Arcy said...

very cool, thanks for sharing!

Kartik Shah said...

Tim,

It throws java.lang.Error assert failed.

I believe you can change visit method to add appropriate exception implementation. e.g. rethrow application exception if you want.

Anonymous said...

Hi,
Would the created annotation work with Java methods ? I guess this is the case, but I'd rather be sure.

Kartik Shah said...

Antoine,

Interesting question...

Annotation is visible to Java classes, if that is your question.

However, I believe the transformation will only be applied to groovy classes.

CyberManiac said...

Hi,
I am trying to test out your code and i don't get any output when i execute the following code. Sorry if it was something silly.
http://groovyconsole.appspot.com/script/68003