...

пятница, 6 декабря 2013 г.

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

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, проверку синтаксиса и рефакторинги в замыкании.




Недостатки:



Для тех, кому не хватает обоих методов, есть третий способ:
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 macro {
println 'Hello from a synthesized method!'
println "Parameter value: $parameter"
}

annotations {}
}
}


Заключение



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

Библиотека вот-вот появится в Maven Central, оставлю ссылку по которой всегда можно найти свежую версию:

search.maven.org/#search%7Cga%7C1%7Cmacro-groovy


Спасибо.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий