Introduction

Abstract Syntax Tree (AST) Transformations are a powerful feature in Groovy that allows you to modify the structure of your code at compile time. This can be used to add new behavior, optimize code, or enforce coding standards. In this section, we will explore what AST Transformations are, how they work, and how you can use them in your Groovy projects.

Key Concepts

  1. Abstract Syntax Tree (AST): A tree representation of the abstract syntactic structure of source code.
  2. Compile-time Metaprogramming: Modifying code during the compilation process.
  3. Annotations: Special markers in the code that can trigger AST Transformations.

Types of AST Transformations

  1. Local Transformations: Applied to specific code elements, such as methods or classes, using annotations.
  2. Global Transformations: Applied to the entire codebase, typically through a custom compiler configuration.

Common AST Transformations in Groovy

  • @ToString: Automatically generates a toString() method.
  • @EqualsAndHashCode: Generates equals() and hashCode() methods.
  • @Immutable: Makes a class immutable.
  • @Delegate: Delegates method calls to another object.

Practical Examples

Example 1: Using @ToString

import groovy.transform.ToString

@ToString
class Person {
    String name
    int age
}

def person = new Person(name: 'John Doe', age: 30)
println person.toString()  // Output: Person(John Doe, 30)

Explanation: The @ToString annotation automatically generates a toString() method for the Person class, which includes the class name and its properties.

Example 2: Using @EqualsAndHashCode

import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode
class Person {
    String name
    int age
}

def person1 = new Person(name: 'John Doe', age: 30)
def person2 = new Person(name: 'John Doe', age: 30)

assert person1 == person2  // True

Explanation: The @EqualsAndHashCode annotation generates equals() and hashCode() methods, allowing for proper comparison of Person objects.

Example 3: Using @Immutable

import groovy.transform.Immutable

@Immutable
class Point {
    int x, y
}

def point = new Point(x: 10, y: 20)
// point.x = 30  // This will throw an error because the class is immutable

Explanation: The @Immutable annotation makes the Point class immutable, meaning its properties cannot be changed after instantiation.

Creating Custom AST Transformations

Step-by-Step Guide

  1. Define the Annotation: Create a custom annotation to mark where the transformation should be applied.
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@interface CustomAnnotation {}
  1. Implement the Transformation: Create a class that implements ASTTransformation.
import org.codehaus.groovy.ast.*
import org.codehaus.groovy.ast.builder.AstBuilder
import org.codehaus.groovy.control.*
import org.codehaus.groovy.transform.*

@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
class CustomTransformation implements ASTTransformation {
    void visit(ASTNode[] nodes, SourceUnit source) {
        // Transformation logic here
    }
}
  1. Register the Transformation: Register the transformation in META-INF/services/org.codehaus.groovy.transform.ASTTransformation.
com.example.CustomTransformation

Example: Adding a Custom Method

import org.codehaus.groovy.ast.*
import org.codehaus.groovy.ast.builder.AstBuilder
import org.codehaus.groovy.control.*
import org.codehaus.groovy.transform.*

@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
class AddMethodTransformation implements ASTTransformation {
    void visit(ASTNode[] nodes, SourceUnit source) {
        if (nodes[0] instanceof AnnotationNode && nodes[1] instanceof AnnotatedNode) {
            AnnotatedNode annotatedNode = (AnnotatedNode) nodes[1]
            if (annotatedNode instanceof ClassNode) {
                ClassNode classNode = (ClassNode) annotatedNode
                MethodNode newMethod = new MethodNode(
                    'customMethod',
                    ACC_PUBLIC,
                    ClassHelper.VOID_TYPE,
                    [] as Parameter[],
                    [] as ClassNode[],
                    new BlockStatement(
                        [new ExpressionStatement(new ConstantExpression('Custom Method Executed'))],
                        new VariableScope()
                    )
                )
                classNode.addMethod(newMethod)
            }
        }
    }
}

Explanation: This custom transformation adds a method named customMethod to any class annotated with @CustomAnnotation.

Practical Exercise

Exercise: Create a Custom AST Transformation

  1. Objective: Create a custom AST Transformation that adds a greet() method to any class annotated with @Greetable.
  2. Steps:
    • Define the @Greetable annotation.
    • Implement the GreetTransformation class.
    • Register the transformation.
    • Test the transformation.

Solution

  1. Define the Annotation:
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@interface Greetable {}
  1. Implement the Transformation:
import org.codehaus.groovy.ast.*
import org.codehaus.groovy.ast.builder.AstBuilder
import org.codehaus.groovy.control.*
import org.codehaus.groovy.transform.*

@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
class GreetTransformation implements ASTTransformation {
    void visit(ASTNode[] nodes, SourceUnit source) {
        if (nodes[0] instanceof AnnotationNode && nodes[1] instanceof AnnotatedNode) {
            AnnotatedNode annotatedNode = (AnnotatedNode) nodes[1]
            if (annotatedNode instanceof ClassNode) {
                ClassNode classNode = (ClassNode) annotatedNode
                MethodNode greetMethod = new MethodNode(
                    'greet',
                    ACC_PUBLIC,
                    ClassHelper.VOID_TYPE,
                    [] as Parameter[],
                    [] as ClassNode[],
                    new BlockStatement(
                        [new ExpressionStatement(new ConstantExpression('Hello, World!'))],
                        new VariableScope()
                    )
                )
                classNode.addMethod(greetMethod)
            }
        }
    }
}
  1. Register the Transformation:
com.example.GreetTransformation
  1. Test the Transformation:
@Greetable
class Greeter {}

def greeter = new Greeter()
greeter.greet()  // Output: Hello, World!

Conclusion

In this section, we explored AST Transformations in Groovy, including their types, common transformations, and how to create custom transformations. AST Transformations are a powerful tool for compile-time metaprogramming, allowing you to modify and enhance your code in ways that are not possible with runtime metaprogramming alone. By mastering AST Transformations, you can create more efficient, maintainable, and expressive Groovy code.

© Copyright 2024. All rights reserved