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.
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.5.0'
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
@CompileStatic
@Phase(Phase.LOCAL.SEMANTIC_ANALYSIS) (1)
class WithLoggingTransformationImpl extends AbstractLocalTransformation<WithLogging, MethodNode> {
@Override
void doVisit(final AnnotationNode annotation, final MethodNode methodNode) {
def before = printlnS("start") (2)
def after = printlnS("end") (3)
A.UTIL.NODE.addAroundCodeBlock(methodNode, before, after) (4)
}
Statement printlnS(String message) {
return A.STMT.stmt(A.EXPR.callThisX("println", A.EXPR.constX(message))) (5)
}
}
1 | The @Phase annotation indicates in which compilation phase this transformation will be applied. |
2 | Building println "start" code, which is wrapped in a org.codehaus.groovy.ast.stmt.Statement |
3 | Building println "end" code, which is wrapped in a org.codehaus.groovy.ast.stmt.Statement |
4 | Building the new method code re-arranging the new and old code in order. |
5 | 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.
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
Examples of checking
-
GContracts: programming by contract
-
Codenarc: Static Analysis
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.
When dealing with the AST, most of the time we will talking about three types of elements:
-
EXPRESSIONS
-
STATEMENTS
-
HIGH LEVEL NODES
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.
1 == 1
-
constant expression 1
-
token ==
-
constant expression 1
ref.myMethod(3)
-
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(booleanExpression) {
println "hello" // statement
}
-
expression to evaluate
-
statement to be executed if the boolean expression evaluates to true
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
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 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.
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 extendingAbstractLocalTransformation
-
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:
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.
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.
|
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.NODE.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:
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.NODE.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:
@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
.
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.NODE.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 |
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.NODE.addCheckTo(A.UTIL.NODE.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.
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 extendingAbstractGlobalTransformation
-
Create as many
transfomers
as you need and then make thegetTransformers
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.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.NODE.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.
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.NODE.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
.
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.
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.NODE.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.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.NODE.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:
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
.
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).
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