Tuesday, 19 November 2013

Generating an Inner class using Groovy AST transformations

Oh man! This was hard to find XD!

During the weekend I've been trying to create a simple AST transformation that generates an inner class. I dove into Groovy's documentation and googled during the last two days, but it seemed nobody has struggled with the same issue.

In the end I achieved the goal, and I thought it would be nice to post it in case anyone could have the same problem :)

The transformation takes a method annotated with @MoveToInner and moves it to an inner class. The inner class will have the name passed as parameter to the annotation.

Here is the code:

package com.github.groovy.astextras.local.inner

import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.AnnotationNode

import org.codehaus.groovy.ast.builder.AstBuilder

import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.CompilationUnit
import org.codehaus.groovy.control.CompilerConfiguration

import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation

/**
 * This transformation takes the method annotated with @MoveToInner and effectively
 * moves it to an inner class which has the name of the value given as value to the
 * former annotation.
 *
 * This way a code like the following:
 * 
 *   package com.github.groovy.astextras.local.inner
 *
 *   class Something {
 *     @MoveToInner("Foo")
 *     def myMethod() {
 *       return "Hello John"
 *     }
 *   }
 * 
* * Will become... * *
 *   package com.github.groovy.astextras.local.inner
 *
 *   class Something {
 *
 *      class Foo {
 *       def myMethod() {
 *          return "Hello John"
 *       }
 *      }
 *
 *   }
 * 
* * */ @GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION) class MoveToInnerAst implements ASTTransformation { static final VALUE = 'value' static final DOLLAR = '$' static final PUBLIC = ClassNode.ACC_PUBLIC void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { if (!checkNodes(astNodes)) return def annotationNode = astNodes[0] def methodNode = astNodes[1] def declaringClass = methodNode.declaringClass def innerClassName = annotationNode.members.getAt(VALUE)?.text def outerClassName = declaringClass.name def outerClassNode = createOuterClass(outerClassName) def innerClassNode = createInnerClass(outerClassName, innerClassName) innerClassNode.addMethod(cloneNode(methodNode)) def compilerConfiguration = sourceUnit.getAST().getUnit().config def compilationUnit = new CompilationUnit(compilerConfiguration).with { addClassNode(outerClassNode) addClassNode(innerClassNode) compile() } } /** * This method checks that the nodes passed as parameters are the ones we * want to visit * * @param astNodes The nodes we may want to process * @return true if the nodes are the ones we were looking for, false otherwise */ def checkNodes(ASTNode[] astNodes) { astNodes && astNodes[0] && astNodes[1] && astNodes[0] instanceof AnnotationNode && astNodes[0].classNode?.name == MoveToInner.class.name && astNodes[1] instanceof MethodNode } /** * This method creates an inner class * * @param qualifiedOuterClassName qualified outer class name (with the name of the package) * @param simpleInnerClassName name of the inner class (without the name of the package) * @return an instance of an InnerClassNode */ def createInnerClass(String qualifiedOuterClassName, String simpleInnerClassName) { def innerClassFullName = qualifiedOuterClassName + DOLLAR + simpleInnerClassName new AstBuilder().buildFromSpec { innerClass(innerClassFullName, PUBLIC) { classNode(qualifiedOuterClassName, PUBLIC) { classNode Object interfaces { classNode GroovyObject } mixins { } } classNode Object interfaces { classNode GroovyObject } mixins { } } }.first() } /** * This method creates an empty class node with the qualified name passed as parameter * * @param qualifiedClassNodeName The qualified name of the ClassNode we want to create * @return a new ClassNode instance */ def createOuterClass(String qualifiedClassNodeName) { new AstBuilder().buildFromSpec { classNode(qualifiedClassNodeName, PUBLIC) { classNode Object interfaces { classNode GroovyObject } mixins { } } }.first() } /** * This method clones the method node passed as parameter * * @param methodNode the MethodNode instance we want to clone from * @return a cloned instance of the node passed as parameter */ def cloneNode(MethodNode methodNode) { new MethodNode( methodNode.name, methodNode.modifiers, methodNode.returnType, methodNode.parameters, methodNode.exceptions, methodNode.code ) } }

The hidden gem of the solution was the following lines:


def compilerConfiguration = sourceUnit.getAST().getUnit().config
        def compilationUnit =
            new CompilationUnit(compilerConfiguration).with {
                addClassNode(outerClassNode)
                addClassNode(innerClassNode)
                compile()
            }

Those lines are responsible to create the .class files for the inner and the outer class. Before using those lines I was unable to make them visible for the rest of the classes. If anybody knows how to do it in any other way, please tell me, I'm all ears.

You can also get the compiler compilation configuration with the shorter expression:

   def compilerConfiguration = classNode.compileUnit.config


 UPDATE (Thanks to Andres Almiray) :

Andres pointed out that the solution was even easier. It was unnecessary to get the compilation unit. The only thing I had to do was:

 void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {

        if (!checkNodes(astNodes)) return

        def annotationNode = astNodes[0]
        def methodNode = astNodes[1]
        def declaringClass = methodNode.declaringClass

        def innerClassName = annotationNode.members.getAt(VALUE)?.text
        def outerClassName = declaringClass.name

        def outerClassNode = createOuterClass(outerClassName)
        def innerClassNode = createInnerClass(outerClassName, innerClassName)

        innerClassNode.addMethod(cloneNode(methodNode))

        methodNode.declaringClass.module.addClass(innerClassNode) 
 }


As I mentioned earlier you can find this code and more AST examples at Github

1 comment:

  1. 4 years later, but, thank you! I was struggling with this!

    ReplyDelete