AST transformations in Groovy Metakoans

Enlightment seekers solve metakoans by filling __ and __() placeholders. They were originally added to each class involved in koan asserts (luckily, there were only two of them). An example of such a class:

class Bike {
    final __ = 'fill_me_in'
    def __() { 'fill_me_in' }
}

As exercise, I moved them to a Groovy AST transformation. I wanted just to annotate a class with @KoanPlaceholders and let the compiler add required methods and fields. Here is the final code of my experiment:

Annotation:

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(['transform.KoanPlaceholdersAstTransformation'])
public @interface KoanPlaceholders {
}

Transformation implementation:

@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
public class KoanPlaceholdersAstTransformation implements ASTTransformation {
    private static final FILL_ME_IN = new ConstantExpression('fill_me_in')
    private static final OBJECT = new ClassNode(Object)
    private static final NO_EXCEPTIONS = [] as ClassNode[]
    private static final NO_PARAMS = [] as Parameter[]
    private static final NO_VARIABLE_SCOPE = null

    @Override
    void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
        if (!nodes) return
        if (nodes.size() < 2) return
        if (!(nodes[0] instanceof AnnotationNode)) return
        if (!(nodes[1] instanceof ClassNode)) return

        ClassNode classNode = nodes[1]
        classNode.addMethod(buildMethodPlaceholder())
        classNode.addField(buildFieldPlaceholder(classNode))
    }

    private buildMethodPlaceholder() {
        def methodBody = new BlockStatement([new ReturnStatement(FILL_ME_IN)], NO_VARIABLE_SCOPE)
        new MethodNode('__', ACC_PUBLIC, OBJECT, NO_PARAMS, NO_EXCEPTIONS, methodBody)
    }

    private buildFieldPlaceholder(owner) {
        new FieldNode('__', ACC_PUBLIC | ACC_FINAL, OBJECT, owner, FILL_ME_IN)
    }
}

Bike after:

@KoanPlaceholders
class Bike {
}

Notes:

  • You have to put the transformation annotation and implementation in a package (don’t work if in the default package)
  • KoanPlaceholdersAstTransformation works if koans are run from the command line (gant), but not from my IDE (IntelliJ Idea). I suppose compilation order is important.
  • Writing a transformation by hand (weaving manually different kinds of ASTNodes) is easier than you think. If you’re beginner, keep your hands off AstBuilder‘s buildFromString/buildFromCode/buildFromSpec. You may think the AstBuilder will sweep the problems under the rug; however, in case of a mis-transformation, you don’t know to deal with it (either you get a compiler exception or simply the transformation doesn’t work). Patching the AST transformation together using the low level API, you learn how the abstract syntax tree is constructed by the Groovy compiler.
  • Load a class into the Groovy console and go to Script/Inspect Ast to learn how AST is built and which nodes are used

And remember:

AST transformations are not scary, give’em a try!

Advertisements