Я начал играть с преобразованиями AST в Groovy несколько дней назад ( обзор главы о преобразованиях AST (GINA2) ). Будучи настолько взволнован этой технологией, я решил написать несколько постов (пошаговых руководств), описывающих, как добавить метапрограммирование времени компиляции в ваш повседневный арсенал.
В этом посте я собираюсь продемонстрировать, как написать локальное преобразование AST. Преобразования, которые я собираюсь написать, немного упрощены, но они действительно работают, и не будет проблемой добавить больше функциональности поверх них.
Быть убежденным в том, что код говорит лучше, чем слова, — это простой тест Спока:
class ExecuteOnceTransformationTest extends Specification{
    def 'should execute a method only once'() {
        when:
        def testObject = new GroovyShell().evaluate('''
            class TestClass {
                int counter = 0
                @victorsavkin.sample1.ExecuteOnce
                void executeOnceMethod(){
                    counter ++
                }
            }
            new TestClass()
        ''')
        then:
        testObject.counter == 0
        when:
        testObject.executeOnceMethod()
        then:
        testObject.counter == 1
        when:
        testObject.executeOnceMethod()
        then:
        testObject.counter == 1
    }
    def 'should generate a compilation error if return type is not void'() {
        when:
        new GroovyShell().evaluate('''
            class TestClass {
                @victorsavkin.sample1.ExecuteOnce
                def executeOnceMethod(){
                    'ReturnValue'
                }
            }
            new TestClass()
        ''')
        then:
        def e = thrown(MultipleCompilationErrorsException)
        e.errorCollector.errorCount == 1
        def firstError = e.errorCollector.errors.first()
        firstError.cause.message.startsWith('ExecuteOnce can be applied only to methods returning void') == true
    }
}
Как вы можете видеть, преобразование, которое я собираюсь написать, может быть применено к методам, и оно изменит AST, чтобы гарантировать, что метод будет выполнен только один раз. Также это преобразование может быть применено только к методам, возвращающим void. Пример из реальной жизни, где его можно использовать: вы можете аннотировать ваш метод init с помощью аннотации @ExecuteOnce, а затем вызывать его во всех ваших методах. Инициализация будет выполнена только один раз. Когда наш тест готов, мы можем приступить к реализации всех частей.
Создать аннотацию
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(['victorsavkin.sample1.ExecuteOnceTransformation'])
@interface ExecuteOnce {
}
Поскольку нам нужно только представить нашу аннотацию во время компиляции, мы используем политику хранения SOURCE. Нетрудно понять, что @GroovyASTTransformationClass определяет класс, который будет использоваться для выполнения нашего преобразования.
Создать класс, реализующий ASTTransformation
@GroovyASTTransformation(phase = SEMANTIC_ANALYSIS)
class ExecuteOnceTransformation implements ASTTransformation {
    ...
    void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
        ...
    }
}
SEMANTIC_ANALYSIS — это самая ранняя фаза, которую можно использовать для локального преобразования. Чем раньше ваш этап, тем меньше деталей реализации у вас есть в вашем AST. Так что если вы хотите манипулировать Groovy-кодом, вы должны использовать ранние фазы. Если вы хотите манипулировать сгенерированным кодом Java (включая сгенерированные методы получения и установки), возможно, лучшим выбором будет INSTRUCTION_SELECTION.
ASTTransformation — очень запутанный интерфейс, так как он используется как локальными, так и глобальными преобразованиями и используется по-разному. В этом примере мы будем использовать astNodes для манипулирования AST и sourceUnit для добавления ошибок компиляции.
Создайте объект спецификации для проверки объекта astNodes.
class ExecuteOnceTransformationSpecification {
    boolean isRightReturnType(astNodes){...}
    SyntaxException getSyntaxExceptionForInvalidReturnType(astNodes){...}
    boolean shouldSkipTransformation(astNodes){...}
}
Чтобы сделать этот пост короче, я не включил код ExecuteOnceTransformationSpecification. Вы можете найти весь исходный код в репозитории github, который я создал для этого поста.
Создать AstFactory для создания частей АСТ
Создание фрагментов AST выглядит для меня немного как черная магия, поэтому я думаю, что было бы неплохо изолировать его внутри этой фабрики.
class ExecuteOnceAstFactory {
    def createGuardIfStatement(fieldName) {
        def ast = new AstBuilder().buildFromString SEMANTIC_ANALYSIS, true, """
            if(! ${fieldName}){
                ${fieldName} = true
            }
        """
        ast[0].statements[0]
    }
    def generatePrivateFieldNode(fieldName) {
        def ast = new AstBuilder().buildFromString SEMANTIC_ANALYSIS, false, """
            class ${fieldName}_Class {
                private boolean ${fieldName} = false
            }
        """
        ast[1].fields.find{it.name == fieldName}
    }
}
Как вы можете видеть, я использую AstBuilder.buildFromString для создания фрагментов AST. Есть много способов создать AST, но buildFromString для меня пока выглядит самым простым. Я использую одну и ту же фазу компиляции SEMANTIC_ANALYSIS для обеих частей. Не волнуйтесь, если не поняли, что делает этот кусок кода. Я бы посоветовал вам запускать тесты с помощью отладчика и просто посмотреть, что будет генерироваться в каждом случае. После нескольких попыток понимание придет к вам.
Соберите все части вместе, чтобы закончить преобразование
@GroovyASTTransformation(phase = SEMANTIC_ANALYSIS)
class ExecuteOnceTransformation implements ASTTransformation {
    private specification = new ExecuteOnceTransformationSpecification()
    private astFactory = new ExecuteOnceAstFactory()
    void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
        if(specification.shouldSkipTransformation(astNodes))
            return
        if(!specification.isRightReturnType(astNodes)){
            sourceUnit.addError specification.getSyntaxExceptionForInvalidReturnType(astNodes)
            return
        }
        MethodNode method = astNodes[1]
        def fieldName = createFieldName(method.name)
        addGuardFieldToClass fieldName, method
        addGuardIfStatementToMethod fieldName, method
    }
    private addGuardFieldToClass(fieldName, method) {
        def field = astFactory.generatePrivateFieldNode(fieldName)
        method.declaringClass.addField field
    }
    private addGuardIfStatementToMethod(fieldName, method) {
        def guardStatement = astFactory.createGuardIfStatement(fieldName)
        addAllStatementsOfMethodIntoGuardIf(guardStatement, method)
        method.code = guardStatement
    }
    private addAllStatementsOfMethodIntoGuardIf(guardIfStatement, method) {
        BlockStatement ifBlock = guardIfStatement.ifBlock
        ifBlock.statements.addAll(method.code.statements)
    }
    private createFieldName(methodName) {
        '$_victorsavkin_samples_execute_once_guard_for_' + methodName
    }
}
Возвращаясь к нашему примеру, если у вас есть этот класс:
class TestClass {
    int counter = 0 
    @victorsavkin.sample1.ExecuteOnce
    void executeOnceMethod(){
        counter ++
    }
}
Скомпилированный код будет выглядеть так:
class TestClass {
    private $_victorsavkin_samples_execute_once_guard_for_executeOnceMethod = false;
    int counter = 0 
    void executeOnceMethod(){
        if(! $_victorsavkin_samples_execute_once_guard_for_executeOnceMethod){
            $_victorsavkin_samples_execute_once_guard_for_executeOnceMethod = true;
            counter ++
        }
    }
}
Это так просто. В реальной жизни мы должны решать проблемы параллелизма, но это усложнит пример, поэтому я не делал этого здесь.
Подводить итоги:
- Написать собственное преобразование AST — не самая простая вещь, но она выполнима и, что также очень важно, тестируема. Кроме того, он позволяет вам изменить семантику языка, что невозможно сделать с помощью метапрограммирования во время выполнения.
 - Я бы посоветовал вам купить Groovy в действии 2, если вы хотите узнать больше об этой теме. Я бы не стал ждать выхода финальной версии и купить ее прямо сейчас.
 - Если вы хотите получить удовольствие от метапрограммирования во время компиляции, взгляните на Спока. Это хорошая иллюстрация того, что можно сделать с помощью преобразований AST и нельзя сделать с помощью метапрограммирования во время выполнения.
 
GitHub Repository
https://github.com/avix1000/AST-Transformations
С http://victorsavkin.com/post/4568615925/compile-time-metaprogramming-in-groovy-part-1