📜 ⬆️ ⬇️

MacroGroovy - working with AST on Groovy has never been easier

image
Recently, you often have to work with Groovy’s powerful capabilities like Compile-time AST Transformations .

Since I do not like excessive dynamics, most of the DSL checks for validity take place at the compilation stage, and we also use a lot of code generation. Therefore, every day we have to deal with the compilation of ASTNodes manually.

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

')
Painfully familiar designs, is not it? Want it to be like this?
 def someVariable = macro { "someValue" } def returnStatement = macro { return new SomeCoolClass($v{ someVariable }) } 


Or even so?
 def constructorCall = macro { new SomeCoolClass($v{ macro { "someValue" } }) } 


This article focuses on my solution to this problem, as close as possible to Groovy's native solution - github.com/bsideup/MacroGroovy



Astbuilder

Groovy 1.7 brought such a seemingly remarkable thing as AstBuilder , which offers us 3 ways to build AST:

AstBuilder.buildFromString

Pass the string with the code, at the output we have the list of ASTNodes:
 List<ASTNode> nodes = new AstBuilder().buildFromString("\"Hello\"") 

Benefits
  • The input is a string that can be taken from anywhere;
  • Does not require an understanding of how ASTNode works;
  • Allows you to specify a CompilePhase;
  • Generates almost 100% valid code;
  • Reliable - does not require a change in your code if the ASTNode structure in Groovy has changed.


disadvantages
  • IDE will not help you with syntax checking;
  • IDE refactorings will not work either;
  • Some entities cannot be created - for example, a class field declaration.


Some of these shortcomings are intended to correct the following method.

AstBuilder.buildFromCode

Pass the closure (aka Closure) with the code, we have a list of nodes at the output:
 List<ASTNode> nodes = new AstBuilder().buildFromCode { "Hello" } 

Benefits (except the advantages of the previous method)
  • The IDE allows you to use autocomplete, syntax checking and refactoring in the closure.

Disadvantages:
  • This method does not solve the problem of the inability to generate a number of entities;
  • Compiles the code, which is why it is not always possible to use cunning constructions, or a class that does not exist;
  • The main drawback for me: the call to buildFromCode requires that the method be called just by creating AstBuilder:
     new AstBuilder().buildFromCode { ... } 

    At the same time, you cannot even take AstBuilder to a separate field or local variable (therefore, the Groovy authors even had to resort to AstTransformation for this AstTransformation in order not to write a lot of code)


For those who lack both methods, there is a third way:

AstBuilder.buildFromSpec

This method takes a closure (by the way, you can vote for my Issue or comment Pull Request so that the beautiful DelegatesTo annotation appears on this method), which is a DSL for building AST:
 List<ASTNode> nodes = new AstBuilder().buildFromSpec { block { returnStatement { constant "Hello" } } } 

Benefits
  • Allows you to use Groovy logic to build nodes;
  • Provides the ability to design almost any existing ASTNode;
  • An important plus, because The AST generation theme in Groovy is not well documented: Fully documented and has extensive use cases in TestCase


disadvantages
  • Sometimes it’s hard to understand exactly what you need to call to get the desired result;
  • Less verbose than calling node constructors, but it still remains so;
  • Strange implementation - for example, some methods accept Class instead of ClassNode, which reduces its use to nothing;
  • Unreliable - AST can change with major releases of the language;
  • You should know exactly how your AST should look like in a specific compilation phase;
  • The IDE does not yet support autocomplete for this DSL (see my comment about the Pull Request).



Combining methods

It is also worth mentioning that you can combine these methods:
 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 {} } } 



Macrogravy

So, after such an extensive review of opportunities, you can ask: So on ... * hm * ... need a fig MacroGroovy?

Consider an example from the post header:
 def someVariable = new ConstantExpression("someValue"); def returnStatement = new ReturnStatement( new ConstructorCallExpression( ClassHelper.make(SomeCoolClass), new ArgumentListExpression(someVariable) ) ); 

See someVariable passed to the argument list constructor? Believe me, this situation is very, very common. And she immediately sweeps away the buildFromCode and buildFromString. So only buildFromSpec remains, but do you remember the list of its flaws? This is where MacroGroovy comes to the rescue:

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


Benefits


disadvantages


By the way, you can still combine buildFromSpec and 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 {} } } 


Leave a link to the test, which shows how MacroGroovy reduces the amount of code at times:
github.com/bsideup/MacroGroovy/blob/master/example/basicExample/src/test/groovy/ru/trylogic/groovy/macro/examples/basic/BasicTest.groovy

Conclusion

Each of the methods has its pros and cons, and I just tried to smooth out the cons of other methods. I would appreciate your help with testing and your pull requests.

The library is available in Maven Central, leaving a link where you can always find the latest version:
search.maven.org/#search%7Cga%7C1%7Cmacro-groovy

Thank.

Source: https://habr.com/ru/post/205084/


All Articles