Как стать автором
Обновить

MacroGroovy — работа с AST на Groovy ещё никогда не была такой простой

Время на прочтение 5 мин
Количество просмотров 5.9K
image
Последнее время часто приходится работать с такой мощной возможностью Groovy как Compile-time AST Transformations.

Так как я не люблю излишнюю динамику, то бОльшая часть проверок DSL на валидность у нас происходит на этапе компиляции, а так же мы используем очень много генерации кода. Поэтому каждый день приходится сталкиваться с составлением ASTNode-ов вручную.

def someVariable = new ConstantExpression("someValue");
def returnStatement = new ReturnStatement(
    new ConstructorCallExpression(
        ClassHelper.make(SomeCoolClass),
        new ArgumentListExpression(someVariable)
    )
);


До боли знакомые конструкции, не правда ли? Хотите, чтобы было вот так?
def someVariable = macro { "someValue" }
def returnStatement = macro { return new SomeCoolClass($v{ someVariable }) }


Или даже так?
def constructorCall = macro { new SomeCoolClass($v{ macro { "someValue" } }) }


В данной статье речь пойдёт о моём решении этой проблемы, максимально близком к родному решению Groovy — github.com/bsideup/MacroGroovy



AstBuilder

Groovy 1.7 принёс такую замечательную, казалось бы, штуку как AstBuilder, который предлагает нам 3 способа построения AST:

AstBuilder.buildFromString

Передаём строку с кодом, на выходе имеем список ASTNode-ов:
List<ASTNode> nodes = new AstBuilder().buildFromString("\"Hello\"")

Преимущества
  • Входные данные — строка, может быть взята откуда угодно;
  • Не требует понимания как устроены ASTNode-ы;
  • Позволяет указывать CompilePhase;
  • Генерирует практически 100% валидный код;
  • Надёжный — не требует изменения в вашем коде если структура ASTNode-ов в Groovy поменялась.


Недостатки
  • IDE не поможет вам с проверкой синтаксиса;
  • рефакторинги в IDE так же не будут работать;
  • Некоторые сущности создать не получится — например объявление поля класса.


Часть этих недостатков призван исправить следующий метод.

AstBuilder.buildFromCode

Передаём замыкание (aka Closure) с кодом, на выходе имеем список нодов:
List<ASTNode> nodes = new AstBuilder().buildFromCode { "Hello" }

Преимущества (кроме преимуществ предыдущего метода)
  • IDE позволяет использовать autocomplete, проверку синтаксиса и рефакторинги в замыкании.

Недостатки:
  • Данный способ не решает проблемы невозможности генерировать ряд сущностей;
  • Компилирует код, из-за чего не всегда получится использовать хитрые конструкции, либо не существующий класс;
  • Самый главный недостаток для меня: вызов buildFromCode требует чтобы метод вызывался именно путём создания AstBuilder-а:
    new AstBuilder().buildFromCode { ... }
    

    При этом Вы даже не сможете вынести AstBuilder в отдельное поле или локальную переменную (поэтому авторам Groovy даже пришлось прибегнуть к AstTransformation для этой AstTransformation чтобы не писать много кода)


Для тех, кому не хватает обоих методов, есть третий способ:

AstBuilder.buildFromSpec

Данный метод принимает в себя замыкание (к слову, вы можете проголосовать за мою Issue или откомментировать Pull Request чтобы на этом методе появилась прекрасная аннотация DelegatesTo), которое представляет из себя DSL для построения AST:
List<ASTNode> nodes = new AstBuilder().buildFromSpec {
    block {
        returnStatement {
            constant "Hello"
        }
    }
}

Преимущества
  • Позволяет использовать логику на Groovy для построения нодов;
  • Предоставлят возможность конструировать практически любую существующую ASTNode-у;
  • Важный плюс, т.к. тема AST generation в Groovy документирована не идеально: Полностью задокументирован и имеет обширные примеры использования в TestCase


Недостатки
  • Иногда сложно понять что именно вам надо вызвать чтобы получить желаемый результат;
  • Менее многословен чем вызовы конструкторов нодов, но всё равно остаётся таковым;
  • Странная реализация — например некоторые методы принимают Class вместо ClassNode, что сводит его использование на нет;
  • Ненадёжен — AST может меняться с мажорными релизами языка;
  • Вы должны точно знать как ваш AST должен выглядеть в конкретной фазе компиляции;
  • IDE пока что (см. мой комментарий по поводу Pull Request-а) не поддерживают autocomplete для данного DSL.



Комбинирование методов

Так же стоит упомянуть что вы можете комбинировать эти методы:
List<ASTNode> result = new AstBuilder().buildFromSpec {
    method('myMethod', Opcodes.ACC_PUBLIC, String) {
        parameters {
            parameter 'parameter': String.class
        }
        exceptions {}
        block {
            owner.expression.addAll new AstBuilder().buildFromCode {
                println 'Hello from a synthesized method!'
                println "Parameter value: $parameter"
            }
        }
        annotations {}
    }
}



MacroGroovy

Итак, после столь обширного обзора возможностей, Вы можете спросить: Так на...*кхм*… фига нужен MacroGroovy?

Рассмотрим пример из шапки поста:
def someVariable = new ConstantExpression("someValue");
def returnStatement = new ReturnStatement(
    new ConstructorCallExpression(
        ClassHelper.make(SomeCoolClass),
        new ArgumentListExpression(someVariable)
    )
);

Видите someVariable, переданную в конструктор списка аргументов? Поверьте мне, такая ситуация встречается очень и очень часто. И она сразу отметает buildFromCode и buildFromString. Значит остаётся только buildFromSpec, но вы помните список его недостатков? Вот тут и приходит на помощь MacroGroovy:

def someVariable = macro { "someValue" };
def returnStatement = macro { return new SomeCoolClass($v{ someVariable }) }


Преимущества
  • Все преимущества первого и второго метода;
  • Не требует создание объекта, относительно которого вызывается метод macro — он доступен во всех классах как Extension Method
  • Внутри переиспользует код из AstBuilder, так что метод надёжный и протестированый;
  • Позволяет использовать логику Groovy внутри кода, т.к. $v принимает в себя замыкание, которое должно вернуть то, что надо поставить на место его вызова;
  • Очень, ОЧЕНЬ компактный:) сравните:
    macro { return mySuperVariable }
    и
    (new AstBuilder()).buildFromCode { return mySuperVariable }.first().expressions.first()
    


Недостатки
  • К сожалению, поле класса вы с помощью macro {} так же не создатите;
  • Нет возможности CompilePhase;


К слову, вы всё так же можете комбинировать buildFromSpec и macro:
List<ASTNode> result = new AstBuilder().buildFromSpec {
    method('myMethod', Opcodes.ACC_PUBLIC, String) {
        parameters {
            parameter 'parameter': String.class
        }
        exceptions {}
        block {
                owner.expression.addAll macro {
                        println 'Hello from a synthesized method!'
                        println "Parameter value: $parameter"
                }
        }

        annotations {}
    }
}


Оставлю ссылку на тест, по которому видно как MacroGroovy уменьшает количество кода в разы:
github.com/bsideup/MacroGroovy/blob/master/example/basicExample/src/test/groovy/ru/trylogic/groovy/macro/examples/basic/BasicTest.groovy

Заключение

Каждый из методов имеет свои плюсы и минусы, и я лишь постарался сгладить минусы других методов. Буду признателен за помощь в тестировании и ваши Pull Request-ы.

Библиотека доступна в Maven Central, оставлю ссылку по которой всегда можно найти свежую версию:
search.maven.org/#search%7Cga%7C1%7Cmacro-groovy

Спасибо.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Что предпочитаете Вы?
6.67% buildFromString 1
0% buildFromCode 0
6.67% buildFromSpec 1
46.67% только ручное создание нодов, только хардкор! 7
60% MacroGroovy :) 9
Проголосовали 15 пользователей. Воздержались 25 пользователей.
Теги:
Хабы:
+16
Комментарии 11
Комментарии Комментарии 11

Публикации

Истории

Работа

Java разработчик
359 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн