Wednesday, July 15, 2015

Custom configuration script ASTs

Nikolay Totomanov asked on the Groovy mailing list how one could add a default constructor (if it doesn't already exist) to all classes. I realized there were no examples on the internet (that I could find anyway) of
  1. How to pass parameters into an AST in a configuration script
  2. How to use a custom AST in a configuration script
So I decided to try to remedy that with this post.

1. AST parameters in a configuration script
Let's say you wanted to do
@groovy.transform.TupleConstructor(includes=['foo'])
in a configuration script. How do you pass the includes? Use a map. The equivalent configuration script would be
withConfig(configuration) {
    ast(groovy.transform.TupleConstructor, includes:['foo'])
}

2. Custom AST in a configuration script
If you wanted to create your own AST and use it in a configuration script, I suggest looking at Groovy as a starting point. Here, I'll use groovy.transform.TupleConstructor and org.codehaus.groovy.transform.TupleConstructorASTTransformation as an example to solve Nikolay's problem. Here is the result
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.AnnotatedNode
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.ConstructorNode
import org.codehaus.groovy.ast.Parameter
import org.codehaus.groovy.ast.stmt.BlockStatement
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.AbstractASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformationClass

@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
public class DefaultConstructorASTTransformation extends AbstractASTTransformation {
    public void visit(ASTNode[] nodes, SourceUnit source) {
        init(nodes, source)
        AnnotatedNode parent = (AnnotatedNode) nodes[1]
        if (parent instanceof ClassNode) {
            ClassNode cNode = (ClassNode) parent
            if (!cNode.getDeclaredConstructor(new Parameter[0])) {  // doesn't already have a default constructor
                cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, new Parameter[0], cNode.EMPTY_ARRAY, new BlockStatement()))
            }
        }
    }
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@GroovyASTTransformationClass("DefaultConstructorASTTransformation")
public @interface DefaultConstructor {}

withConfig(configuration) {
    ast(DefaultConstructor)
}