Asteroid is a set of utilities to make it easier to develop Groovy AST transformations.

1. What is Asteroid

AST transformations, have been historically a hard topic in Groovy. Asteroid is a set of utilities and ideas trying to reduce the complexity of dealing with transformations.

Apache

The Asteroid project is open sourced under the Apache 2 License.

If you have never done any AST transformation I’d recommend you to take a look both the Groovy documentation and the theory chapter. If you already now the stuff then read on.

At the moment Asteroid development is in an alpha state. Please check the changelog file to follow the progress of the project.

2. Show me the code

2.1. Gradle

In order to use Asteroid in your Groovy project just add the jcenter repository:

repositories {
    jcenter()
}

Then you can add the dependency to your project:

compile 'com.github.grooviter:asteroid:0.2.5'

2.2. Example

To show the benefit of using Asteroid, I will be following the tutorial about local transformation available at the Groovy official site. The code of the following example is available at the asteroid-test module at Github.

Given a code like the following:

@WithLogging
def greet() {
    println "Hello World"
}

greet()

We would like to print a start and stop message along with the message printed by the method itself. So in this example we’ll be expecting an output like:

start greet
Hello World
stop greet

For a local transformation only two things are required:

  • The annotation used as a marker. In this example the @WithLogging annotation

  • The transformation implementation

Lets see first the @WithLogging annotation declaration:

package asteroid.local.samples

import asteroid.Local

@Local(
    value   = WithLoggingTransformationImpl, (1)
    applyTo = Local.TO.METHOD)               (2)
@interface WithLogging { }
1 The transformation implementation class
2 This annotation will be applied to method elements.
By default @Local annotation assumes the annotation is applied to a type (classes). So if you are using the annotation for a type then you could omit value and applyTo attributes an write just the class of the transformation like this: @Local(ImplementationClass).

Now it’s time to implement the transformation. The transformation should be an instance of asteroid.local.AbstractLocalTransformation. We have to extend AbstractLocalTransformation and provide two generic arguments:

  • The annotation class used to mark our transformation: WithLogging

  • The type of nodes that will be affected by this transformation. In this example org.codehaus.groovy.ast.MethodNode

package asteroid.local.samples

import asteroid.A
import asteroid.Phase
import asteroid.AbstractLocalTransformation

import groovy.transform.CompileStatic

import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.stmt.Statement
import org.codehaus.groovy.ast.expr.Expression

@CompileStatic
@Phase(Phase.LOCAL.SEMANTIC_ANALYSIS) (1)
class WithLoggingTransformationImpl extends AbstractLocalTransformation<WithLogging, MethodNode> {

    @Override
    void doVisit(final AnnotationNode annotation, final MethodNode methodNode) {
        def oldCode   = methodNode.code   (2)
        def startCode = printlnS("start") (3)
        def endCode   = printlnS("end")   (4)

        methodNode.code = A.STMT.blockS(startCode, oldCode, endCode) (5)
    }

    Statement printlnS(String message) {
        return A.STMT.stmt(A.EXPR.callThisX("println", A.EXPR.constX(message))) (6)
    }
}
1 The @Phase annotation indicates in which compilation phase this transformation will be applied.
2 Storing temporary the old code we want to transform which is an org.codehaus.groovy.ast.stmt.Statement
3 Building println "start" code, which is wrapped in a org.codehaus.groovy.ast.stmt.Statement
4 Building println "end" code, which is wrapped in a org.codehaus.groovy.ast.stmt.Statement
5 Building the new method code re-arranging the new and old code in order. We are adding all previous in a BlockStatement in the order we want them to execute.
6 Building a generic println constantValue expression

The @CompileStatic annotation is not required it’s only used here to highlight that all the code used in this transformation is safely typed and can be optimized by this annotation.

3. Overview

At the moment Asteroid is composed by two main groups, abstractions to reduce the complexity of creating a new transformation, and utility classes helping to create new Abstract Syntaxt Tree nodes.

diag 4608b07edeacc2d2670155ae10dc179a

3.1. Transform abstractions

So far abstractions used to deal with the AST were too low level. For instance, you needed to check whether the nodes passed to your transformation were the ones you wanted to act over or not, and then proceed.

Asteroid tries to provide higher abstractions in order to reduce some of the boiler plate code, and make the developer to focus on the transformation only.

3.2. AST nodes functions

The other main part of Asteroid are functions dealing directly with AST nodes. Functions responsible for modifying AST nodes.

They’re divided in four groups:

  • Expressions: Functions responsible for creating expressions

  • Statements: Functions responsible for creating statements

  • Nodes: Builders responsible for creating high level nodes

  • Utils: Functions responsible for querying and querying any type of nodes

With the upcoming groovy-macro module in Groovy 2.5.0 most of the code in Asteroid, used for creating expressions and statements may be gone for good in favor of the macro method.

3.2.1. The A class

All functions available in Asteroid are accessible through the asteroid.A class.

Check javadoc: asteroid.A

4. Theory

What can you do with an AST transformation ?

  • Transform the Abstract Syntax Tree by adding, or removing elements from it

  • Check the structure, or semantics of the Abstract Syntax Tree and do something about it

Examples of adding / removing

  • Groovy: Code generation transformations: @ToString, @Immutable…​

  • Spock: transforms label statements

  • Swissknife: reduces boilerplate code in Android dev

  • Grails: also saves you from typical web boilerplate code

grails spock swissknife griffon

Examples of checking

codenarc

Transformations can be of two types:

Local

  • Relative to the context they are applied to.

  • That context is marked (annotation)

  • Compilation phases are limited

Global

  • Global AST transformations are applied to all source code

  • Compilation phases are less limited

  • Need an extra descriptor file

4.1. AST

Abstract Syntax Tree (or AST from now on) is the tree-like representation of the code the compiler needs in order to generate the bytecode that will be used later by the JVM.

diag babc24851067e5feb8fc8776b5bb8948

When dealing with the AST, most of the time we will talking about three types of elements:

  • EXPRESSIONS

  • STATEMENTS

  • HIGH LEVEL NODES

diag 6c894a44eec094de5339c817017775e7

4.1.1. Expressions

An expression is a combination of one or more explicit values, constants, variables, operators, and functions that the programming language interprets and computes to produce another value.

BinaryExpression
1 == 1
diag 836fe3d4bdb17e1b2addb10be8c7ea6d
  • constant expression 1

  • token ==

  • constant expression 1

Method call expression
ref.myMethod(3)
diag c4448142060fbd4b605eb6d5ae5614ca
  • variable expression ref

  • constant myMethod

  • param expression 3

4.1.2. Statements

In computer programming, a statement is the smallest standalone element of an imperative programming language that expresses some action to be carried out. A statement may have expressions.

If Statement
if(booleanExpression) {
 println "hello" // statement
}
  • expression to evaluate

  • statement to be executed if the boolean expression evaluates to true

Block Statement
public void main(String[] args) { // block starts
  // this is inside a block statement
} // block ends
  • A block statement is easily recognized by curly braces

  • It is built from other statements containing expressions

Block Statement (Cont.)
public String greetings() {
    return "Hello Greach"
}

This block statement contains a return statement receiving a constant expression Hello Greach.

4.1.3. High level Nodes

Is how our program is structured. They group statements and expressions:

  • classes

  • methods

  • fields

  • properties

  • …​

Class Node
class A { // ClassNode
   String greetings // FieldNode

   String hello() { // MethodNode

   }
}
  • ClassNode may contain: methods, fields…​

  • MethodNode may contain statements, and expressions

  • …​

Therefore…​

class A { // ClassNode

   String hello() // MethodNode
   { // blockStatement {

       return "Hello" // returnStatement(constantExpression)

    } // }
}

5. Local Transformations

Local AST transformations are relative to the context they are applied to. In most cases, the context is defined by an annotation that will define the scope of the transform. For example, annotating a field would mean that the transformation applies to the field, while annotating the class would mean that the transformation applies to the whole class.
— Groovy official site

5.1. Overview

In order to create a local transformation you need to:

  • Create an annotation annotated by @Local

  • Create an implementation of the transformation extending AbstractLocalTransformation

  • Your implementation should be annotated by @Phase with the proper local compilation phase value set.

5.2. @Local

In a local transformation you normally use an annotation to mark those parts of the code you want to transform: classes, methods…​ That annotation should be annotated as well to tell the compiler that is going to be used as a transformation marker.

You can use @Local to annotate a marker annotation. The only mandatory argument is the AST implementation class. Implementation classes should always extend asteroid.local.AbstractLocalTransformation class.

package asteroid.local.samples

import asteroid.Local

@Local(AsListImpl)
@interface AsList { }

If @Local annotation does not indicate which type of element is allowed to annotate by the attribute appliedTo then is supposed to be used over an element of type TYPE, meaning it will be applied over an entire class.

Underneath the @Local annotation is doing:

Local annotation transformation
Figure 1. Local annotation transformation

5.3. applyTo

applyTo attribute is used when the transformation is applied to any element type other than TYPE: a method, annotation, field…​etc.

package asteroid.local.samples

import asteroid.Local

@Local(
    value   = WithLoggingTransformationImpl, (1)
    applyTo = Local.TO.METHOD)               (2)
@interface WithLogging { }
1 This annotation will be applied to method elements.
2 The class of the AST transformation implementation

Sometimes you may want to target more than one element: a class and a method, or a parameter and a local variable. Then you can use TO.ANNOTATED that means that the transformation could be applied to any annotated node.

annotation
package asteroid.local.samples

import asteroid.Local

@Local(value = NameCheckerImpl, applyTo = Local.TO.ANNOTATED)
@interface NameChecker {
    String value()
}
Most of the times a single node type is targeted. That idea was kept in mind when creating the applyTo parameter. That’s why applyTo doesn’t receive a list of possible targets. ANNOTATED is the exception to the rule that allows to do so.

Therefore in your implementation you should use the AnnotatedNode type.

The following example of method overriding only works with dynamic Groovy, if you statically compiled the code it would only be executing getName(AnnotatedNode). So if you compile your code statically you should probably be using a instanceof to dispatch your actions.
implementation
package asteroid.local.samples

import asteroid.A
import asteroid.AbstractLocalTransformation
import asteroid.Phase
import org.codehaus.groovy.ast.AnnotatedNode
import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.FieldNode

/**
 * Checks whether an {@link AnnotatedNode} follows the defined pattern in the
 * pattern property
 *
 * @since 0.2.5
 */
@Phase(Phase.LOCAL.INSTRUCTION_SELECTION)
class NameCheckerImpl extends AbstractLocalTransformation<NameChecker, AnnotatedNode> { (1)
    @Override
    void doVisit(AnnotationNode annotation, AnnotatedNode annotated) { (2)
        String pattern = A.UTIL.ANNOTATION.getStringValue(annotation)
        String nodeText = getName(annotated)
        Boolean matches = nodeText ==~ pattern

        if (!matches) {
            addError 'Pattern doesn\'t match annotated name', annotated
        }
    }

    String getName(ClassNode classNode){  (3)
        return classNode.name
    }

    String getName(FieldNode fieldNode) { (4)
        return fieldNode.name
    }

    String getName(AnnotatedNode annotatedNode) { (5)
        addError "Pattern doesn't match annotated name", annotatedNode
    }
}
1 You expect to receive any type of node extending AnnotatedNode
2 Receiving the annotated node as a parameter
3 Executing logic if the node is of type ClassNode
4 Executing logic if the node is of type FieldNode
5 Executing logic if the node is of any other type extending AnnotatedNode

Then you can use it in any annotated node:

example
package asteroid.local.samples

@NameChecker('.*Subject')
class CheckerSubject {

  @NameChecker('.*Field')
  String stringField = 'doSomething'
}

assert new CheckerSubject().stringField == 'doSomething'

5.4. AbstractLocalTransformation

asteroid.local.AbstractLocalTransformation exists to avoid some of the defensive code that you would normally write at the beggining of an AST transformation.

When coding an AST transformation you always check that the first node is an AnnotationNode and the second is the type of ASTNode you expected to be annotated by the first node. Instead of coding that you can use AbstractLocalTransformation.

Lets say I have an annotation @ToMD5. That annotation can only be used in elements of type FIELD:

package asteroid.local.samples

import asteroid.Local

@Local(value = ToMD5Impl, applyTo = Local.TO.FIELD)
@interface ToMD5 { }

I would like to create a method for every field annotated by ToMD5 returning the MD5 signature of the content of that field.

In order to implement that I’m using AbstractLocalTransformation:

package asteroid.local.samples

import asteroid.A
import asteroid.Phase
import asteroid.AbstractLocalTransformation

import groovy.transform.CompileStatic

import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.FieldNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.stmt.BlockStatement

@CompileStatic
@Phase(Phase.LOCAL.SEMANTIC_ANALYSIS) (1)
class ToMD5Impl extends AbstractLocalTransformation<ToMD5, FieldNode> { (2)

    @Override
    void doVisit(AnnotationNode annotation, FieldNode node) { (3)
        BlockStatement block  = buildMethodCode(node.name)
        MethodNode methodNode = A.NODES.method("${node.name}ToMD5")
            .modifiers(A.ACC.ACC_PUBLIC)
            .returnType(String)
            .code(block)
            .build()

        A.UTIL.CLASS.addMethod(node.declaringClass, methodNode)
    }

    private BlockStatement buildMethodCode(final String name) {
        A.STMT.blockSFromString """
            return java.security.MessageDigest
                .getInstance('MD5')
                .digest(${name}.getBytes())
                .encodeHex()
                .toString()
        """
    }

}
1 Declaring when to apply this transformation with the annotation @Phase and the correspondent compilation phase.
2 Creating a class extending AbstractLocalTransformation and declaring that the annotation and the affected node type are ToMD5 and FieldNode respectively
3 The override method declares the correct generic type FieldNode.

From this line on you don’t have to be worried about casting first and second node passed to your transformation anymore.

Sometimes it comes handy to get a reference to org.codehaus.groovy.control.SourceUnit. In previous versions SourceUnit was passed as argument, but it forced to add an import whether you used or not. Now it’s present as a class field. Probably in future release won’t be available directly but through specific functions.

5.5. @Phase

@Phase is a required annotation for both global and local transformations that indicates in which compilation phase this transformation will be applied.

Lets see how @Phase annotation is processed in a local transformation:

Local Transformation
Figure 2. Local Transformation

@Phase annotation needs a value of type org.codehaus.groovy.control.CompilePhase enum, but because sometimes is hard to remember which phases are available depending on which type of transformation we are implementing and it would add one more import to our code, Asteroid provides a shortcut to these values:

  • asteroid.Phase.LOCAL

  • asteroid.Phase.GLOBAL

This way is always easier to remember how to get the proper compilation phase. Here’s an example:

package asteroid.local.samples

import asteroid.A
import asteroid.Phase
import asteroid.AbstractLocalTransformation

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

@Phase(Phase.LOCAL.SEMANTIC_ANALYSIS) (1)
class AsListImpl extends AbstractLocalTransformation<AsList, ClassNode> {

    @Override
    void doVisit(AnnotationNode annotation, ClassNode classNode) {
        classNode.superClass = A.NODES.clazz(ArrayList).build()
    }
}
1 This is a local transformation to be applied during SEMANTIC_ANALYSIS phase.

This transformation will be applied to those ClassNode instances annotated with @AsList.

Groovy friendly

When used over a local transformation implementation in Groovy, apart from indicating the compilation phase, underneath, it saves some of the boilerplate code needed to implement an instance of asteroid.local.AbstractLocalTransformation.

Although you can create an AbstractLocalTransformation in plain Java, you then will have to annotate your transformations like the old days.

5.6. Compilation errors

If at some point you would like to stop the compilation process the best approach is to use addError method. This method is available in both AbstractLocalTransformation and AbstractGlobalTransformation.

package asteroid.local.samples

import asteroid.Phase
import asteroid.AbstractLocalTransformation

import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.AnnotationNode

@CompileStatic
@Phase(Phase.LOCAL.SEMANTIC_ANALYSIS)
class GrumpyImpl extends AbstractLocalTransformation<Grumpy, ClassNode> {

    @Override
    void doVisit(AnnotationNode annotation, ClassNode clazz) {
        addError("I don't like you Argggg!!!!! (said the Grumpy transformation)", clazz)
    }
}

5.7. Checks

There are many times when you have to check if all precoditions are correct before applying a given transformation. Without this sanity check, many things could go wrong. Checks labels are an effort to avoid boiler plate code when checking the AST state. They are inspired in Spock blocks.

By default checks labels are available in Asteroid local transformations. All you have to do is to structure your code using labels check and then.

Here’s an example, it’s a bit silly but I think it will easy to understand. We have a annotation called @Serializable.

The transformation SerializableImpl will make all classes annotated with @Serializable to implement java.io.Serializable.

package asteroid.local.samples

import asteroid.Local

@Local(SerializableImpl)
@interface Serializable {}

As constraints I want to make sure:

  • The annotated class package name should should start by 'asteroid'

  • The annotated class can only have two method at most

package asteroid.local.samples

import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.AnnotationNode

import asteroid.A
import asteroid.Phase
import asteroid.AbstractLocalTransformation

@CompileStatic
@Phase(Phase.LOCAL.INSTRUCTION_SELECTION)
class SerializableImpl extends AbstractLocalTransformation<Serializable, ClassNode> {

    @Override
    void doVisit(AnnotationNode annotation, ClassNode classNode) {
        check: 'package starts with asteroid'
        classNode.packageName.startsWith('asteroid') (1)

        check: 'there are least than 2 methods'
        classNode.methods.size() < 2 (2)

        then: 'make it implements Serializable and Cloneable'
        A.UTIL.CLASS.addInterfaces(classNode, java.io.Serializable, Cloneable) (3)
    }
}
1 Checking the annotated class belongs to a certain package
2 Checking that the annotated node has less than two methods
3 Transformation code
Limitations

Please notice at the moment checks only have a very limited functionality. They only allow a one-line expression. And these expressions can only see doVisit parameter values.

To prove it, there’s a test with an annotated class having two methods:

    void testFailsBecauseNumberOfMethods() {
        shouldFail '''
        package asteroid.local.samples

        @Serializable
        class A {
            def a() {}
            def b() {}
        }
        '''
    }

And the test…​ passes :)

5.7.1. Your own transformations

If you would like to add this functionality in your project, you can use Asteroid utility functions to inject this behavior in your code.

A.UTIL.CHECK.addCheckTo(A.UTIL.CLASS.findMethodByName(annotatedNode, METHOD_DOVISIT));

This call is taken from Asteroid local transformations. Checking is added to method doVisit.

6. Global Transformations

Global AST transformation are similar to local one with a major difference: they do not need an annotation, meaning that they are applied globally, that is to say on each class being compiled. It is therefore very important to limit their use to last resort, because it can have a significant impact on the compiler performance.
— Groovy official site

6.1. Overview

Asteroid suggest a certain way of creating global AST transformations. Instead of creating a global transformation and manipulate the SourceUnit directly, an Asteroid global transformation only holds references to code transformers.

In order to create a global transformation you need to:

  • Create an implementation of the transformation extending AbstractGlobalTransformation

  • Create as many transfomers as you need and then make the getTransformers method from your transformation to return the classes of those transformers.

  • Your implementation should be annotated by @Phase with the proper local compilation phase value set.

  • Add a transformation descriptor in your classpath to tell the compiler where it can find your transformation

6.2. Example

package asteroid.global.samples

import static asteroid.Phase.GLOBAL

import groovy.transform.CompileStatic

import asteroid.Phase
import asteroid.AbstractGlobalTransformation
import asteroid.transformer.Transformer

@CompileStatic
@Phase(GLOBAL.CONVERSION) (1)
class AddTransformation extends AbstractGlobalTransformation { (2)

    @Override
    List<Class<Transformer>> getTransformers() {
        return [AddPropertyToInnerClass, AddTraitTransformer] (3)
    }
}
1 Declaring class as a global AST transformation
2 Extending asteroid.global.GlobalTransformationImpl
3 Adding asteroid.global.AbstractClassNodeTransformer classes

A global transformation needs to be annotated with the @GlobalTransformation annotation, then it should extend GlobalTransformationImpl and finally to provide a list of the transformers that will eventually transform the code.

In this example the code of the transformer is the following:

package asteroid.global.samples

import groovy.transform.CompileStatic

import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.ast.expr.Expression
import org.codehaus.groovy.ast.ClassNode

import asteroid.A
import asteroid.transformer.AbstractClassNodeTransformer

@CompileStatic
class AddPropertyToInnerClass extends AbstractClassNodeTransformer { (1)

    AddPropertyToInnerClass(final SourceUnit sourceUnit) {
        super(sourceUnit,
              A.CRITERIA.byClassNodeNameContains('AddTransformerSpecExample$Input')) (2)
    }

    @Override
    void transformClass(final ClassNode target) { (3)
        A.UTIL.CLASS.addInterfaces(target, java.io.Serializable)
    }
}
1 Because this transformer targets class nodes it extends ClassNodeTransformer
2 Every ClassNodeTransformer requires a SourceUnit and a criteria to filter class nodes
3 Then the programmer should only be focused on develop de transformation within the transformClass method

Finally add the descriptor file to your classpath at META-INF/services/ the descriptor file should be named org.codehaus.groovy.transform.ASTTransformation, and it will contain the fully qualified name of your AST transformation implementation: asteroid.global.samples.AddTransformationImpl.

If you are using Gradle or Maven the transformation descriptor will be normally found at src/main/resources/META-INF/org.codehaus.groovy.transform.ASTTransformation.
Remember that for any new global transformation you should add the new qualified class in a new line.

6.3. Transformers

Because a global AST transformation can act over the whole source code, we use transformers to focus only on certain parts of it. Transformers theirselves declare which type of nodes they are interested in, but, they also use criterias to narrow the search.

6.3.1. ClassNodeTransformer

This type of transformers only focuses on transforming a specific set of ClassNode instances from the AST.

ClassNodeTransformer
class AddImportTransformer extends AbstractClassNodeTransformer { (1)

    public AddImportTransformer(final SourceUnit sourceUnit) {
        super(sourceUnit,
              A.CRITERIA.byAnnotationSimpleName('AddImport')) (2)

    }

    /**
     * {@inheritDocs}
     */
    @Override
    void transformClass(final ClassNode target) { (3)
        A.UTIL.CLASS.addImport(target, groovy.json.JsonOutput) (4)
    }
}
1 Extending ClassNodeTransformer we are only interested in ClassNode instances
2 Then we use a criteria to declare we’re only interested in ClassNode instances annotated by an annotation which has a simple name AddImport
3 Overriding the transformClass method we will be receiving the expected ClassNode
4 We don’t return anything because we are modifying the node, we are not supposed to replace it in the AST.
Why simple name ? Well depending on the compilation phase you are targeting the information about the class may be not available, that means it’s fully qualified name.
Transforming an AST node here means to add/remove elements from the AST node.

6.3.2. Expression transformers

This type of transformers only focuses on replacing certain expressions found along the AST.

In the following example, we are interested in replacing all method calls xxx() by a constant number 1.

ExpressionTransformer
class ChangeTripleXToPlusOne
    extends AbstractExpressionTransformer<MethodCallExpression> { (1)

    ChangeTripleXToPlusOne(final SourceUnit sourceUnit) {
        super(MethodCallExpression,
              sourceUnit,
              A.CRITERIA.byExprMethodCallByName('xxx')) (2)
    }

    @Override
    Expression transformExpression(final MethodCallExpression target) { (3)
        return A.EXPR.constX(1)  (4)
    }
}
1 We declare this transformer is focused on MethodCallExpression elements
2 We declare we are only interested on method calls with name xxx
3 Overriding the transformExpression operation we will be receiving the expected node
4 Finally will be returning the expression that will replace the former expression in the AST
It’s very important to notice the fact that we are here replacing an expression cause expressions are considered as values.

7. Criterias

Every time we want to apply a transformation we want to target specific nodes:

  • A method with a specific annotation

  • A class with a specific annotation

  • A method with a specific name

  • A method with a specific name and a specific annotation

  • …​

Instead of holding a node reference and start calling methods from that reference in order to find out which is the node we are interested in, we can use criterias.

Criterias are available directly as static nodes in the asteroid.Criterias or via A.CRITERIA

7.1. Transformers

Lets say we would like to apply a given transformation to a specific set of classes annotated with a certain annotation.

Criteria
class AddImportTransformer extends AbstractClassNodeTransformer { (1)

    public AddImportTransformer(final SourceUnit sourceUnit) {
        super(sourceUnit,
              A.CRITERIA.byAnnotationSimpleName('AddImport')) (2)

    }

    /**
     * {@inheritDocs}
     */
    @Override
    void transformClass(final ClassNode target) { (3)
        A.UTIL.CLASS.addImport(target, groovy.json.JsonOutput) (4)
    }
}
1 Using a class node transformer
2 Looking for a class node annotated with an annotation with name AddImport

What about looking for a ClassNode representing an inner class having a specific name.

package asteroid.global.samples

import groovy.transform.CompileStatic

import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.ast.expr.Expression
import org.codehaus.groovy.ast.ClassNode

import asteroid.A
import asteroid.transformer.AbstractClassNodeTransformer

@CompileStatic
class AddPropertyToInnerClass extends AbstractClassNodeTransformer { (1)

    AddPropertyToInnerClass(final SourceUnit sourceUnit) {
        super(sourceUnit,
              A.CRITERIA.byClassNodeNameContains('AddTransformerSpecExample$Input')) (2)
    }

    @Override
    void transformClass(final ClassNode target) { (3)
        A.UTIL.CLASS.addInterfaces(target, java.io.Serializable)
    }
}
1 Again looking for a class node
2 With a name containing a specific Outer$Inner class name

Now a different example. Instead of targeting a ClassNode we would like to find a specific method call expression. We know what is the name of the method we are calling, that’s why we use methodCallByName criteria here:

ExpressionTransformer
class ChangeTripleXToPlusOne
    extends AbstractExpressionTransformer<MethodCallExpression> { (1)

    ChangeTripleXToPlusOne(final SourceUnit sourceUnit) {
        super(MethodCallExpression,
              sourceUnit,
              A.CRITERIA.byExprMethodCallByName('xxx')) (2)
    }

    @Override
    Expression transformExpression(final MethodCallExpression target) { (3)
        return A.EXPR.constX(1)  (4)
    }
}

7.2. and / or

Sometimes using only one criteria could be limiting, sometimes we may want to combine two or more criterias at once. For that purpose you can use A.CRITERIA.and and A.CRITERIA.or. Check next example:

class AddLoggerTransformer extends AbstractClassNodeTransformer {

    static final Closure<Boolean> CRITERIA = (1)
        A.CRITERIA.with {
            and(byClassNodeNameStartsWith('asteroid.global.samples'),
                or(byClassNodeNameContains('Logger'),
                   byClassNodeNameEndsWith('Example')))
        }

    AddLoggerTransformer(final SourceUnit sourceUnit) {
        super(sourceUnit, CRITERIA) (2)
    }

    @Override
    void transformClass(final ClassNode target) { (3)
        target.addAnnotation(A.NODES.annotation(Log).build())
    }
}
1 Criteria looks for class nodes with name starting with asteroid.global.samples and nodes containing Logger or nodes ending with Example
2 Applying the criteria in the constructor
3 Adding a note to filtered nodes

7.3. As predicates

Although criteras were meant to be used in transformers, the way they were designed makes them perfectly valid to be used as predicates when filtering lists of AST nodes. The following example ask for all method nodes from a given class node and then as any Groovy list tries to find all methods that comply with the filter passed as parameter: all methods annotated with the Important annotation and with a name starting with get.

Criterias in Local Transformations
        List<MethodNode> notSoImportantMethods = classNode
            .methods
            .findAll(A.CRITERIA.and(A.CRITERIA.byAnnotation(Important),
                                    A.CRITERIA.byMethodNodeNameStartsWith('get')))

Because criterias are just closures that eventually return a boolean value they fit perfectly in this scenario.

8. Fluent API

With the upcoming groovy-macro module in Groovy 2.5.0 most of the code in Asteroid, used for creating expressions and statements may be gone for good in favor of the macro method.

The main goal of this project is to have a unified way to access all AST APIs through a single entry point. That entry point is asteroid.A.

  • NODES: Create instances of org.codehaus.groovy.ast.ASTNode

  • EXPRESSIONS: Create instances of org.codehaus.groovy.ast.expr.Expression

  • STATEMENTS: Create instances of org.codehaus.groovy.ast.stmt.Statement

  • MODIFIERS: asteroid.A.ACC

  • CHECKERS: asteroid.A.CHECK. Access to checkers.

  • UTILS: asteroid.A.UTIL.

The project has been developed having in mind to get the general idea reading this documentation and then checking the specifics using the javadoc.

Please check out the javadoc here

8.1. Nodes

Check javadoc: asteroid.A.NODES

This is entry point accesses to a set of builders to create instances of org.codehaus.groovy.ast.ASTNode. All builders in asteroid.A.NODES follow the same API:

A.NODES.annotation("MyAnnotation") (1)
       .member(...) (2)
       .xxxxxx(...)
       .yyyyyy(...)
       .build()  (3)
1 Creates a builder of a specific type of ASTNode.
2 Then each node builder has a set of associated methods. E.g: annotation has member(…​) to add annotation members…​
3 Once the node has been configured is time to create the instance calling the build() method of the current builder.

For instance, the following code:

A.NODES.annotation("MyAnnotation")
       .member("message", A.EXPR.constX("hello"))
       .member("sort", A.EXPR.constX("desc"))
       .build()

Will produce the following annotation:

@MyAnnotation(message = "hello", sort = "desc")
The nodes related javadoc has the same structure. Normally you’ll see the AST and the resulting code explained.

8.2. Expressions

Check javadoc: asteroid.A.EXPR

This entry point accesses to a set of methods returning instances of org.codehaus.groovy.ast.expr.Expression.

Unlike the asteroid.A.NODES this entry point only has methods creating expressions directly. So bear in mind that all methods from asteroid.A.EXPR will return instances of org.codehaus.groovy.ast.Expression.

For instance if we would like to create an expression like:

println "hello"

We should code:

A.EXPR.callThisX( (1)
    "println",
    A.EXPR.constX("hello") (2)
)
1 Creates an instance of org.codehaus.groovy.ast.expr.MethodCallExpression
2 Creates an instance of org.codehaus.groovy.ast.expr.ConstantExpression used as method call argument.
Why using callThisX when coding println ? This is because println is added to all JDK objects through the DefaultGroovyMethods object.

8.3. Statements

Check javadoc: asteroid.A.STMT

In computer programming a statement is the smallest standalone element of an imperative programming language that expresses some action to be carried out. It is an instruction written in a high-level language that commands the computer to perform a specified action.[1] A program written in such a language is formed by a sequence of one or more statements. A statement may have internal components (e.g., expressions).
— Wikipedia

This entry point accesses to a set of methods returning instances of org.codehaus.groovy.ast.stmt.Statement. It follows the same design as expressions. Each method returns an statement. For instance, a return statement:

return 1

Can be written as:

A.STMT.returnS(A.EXPR.constX(1))

8.4. Modifiers

Check javadoc: asteroid.A.ACC

When creating a node we may want to restrict its visibility, or mark it as static…​etc. To do that each node has a method called setModifiers(int). That value can be built with one or more values from this entry point:

A.ACC.ACC_PUBLIC // public

8.5. Utils

Check javadoc: asteroid.A.UTIL