Add groovy-macro documentation (closes #439)
Project: http://git-wip-us.apache.org/repos/asf/groovy/repo Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/cff7a3c3 Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/cff7a3c3 Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/cff7a3c3 Branch: refs/heads/parrot Commit: cff7a3c3d212f6d64058a427d34170bd30777fa2 Parents: 9e1a65e Author: Mario Garcia <mario.g...@gmail.com> Authored: Fri Oct 7 01:27:32 2016 +0200 Committer: paulk <pa...@asert.com.au> Committed: Fri Oct 14 20:53:36 2016 +1000 ---------------------------------------------------------------------- build.gradle | 2 + src/spec/doc/core-metaprogramming.adoc | 237 ++++++++++++++++++- .../ASTMatcherFilteringTest.groovy | 100 ++++++++ .../ASTMatcherTestingTest.groovy | 120 ++++++++++ .../test/metaprogramming/MacroClassTest.groovy | 101 ++++++++ .../metaprogramming/MacroExpressionTest.groovy | 92 +++++++ .../metaprogramming/MacroStatementTest.groovy | 123 ++++++++++ .../MacroVariableSubstitutionTest.groovy | 93 ++++++++ 8 files changed, 865 insertions(+), 3 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/groovy/blob/cff7a3c3/build.gradle ---------------------------------------------------------------------- diff --git a/build.gradle b/build.gradle index e82c4ab..fb340d9 100644 --- a/build.gradle +++ b/build.gradle @@ -214,6 +214,7 @@ dependencies { examplesCompile project(':groovy-test') examplesCompile project(':groovy-swing') + examplesCompile "org.apache.lucene:lucene-core:$luceneVersion" examplesCompile "org.apache.lucene:lucene-analyzers-common:$luceneVersion" examplesCompile "org.apache.lucene:lucene-queryparser:$luceneVersion" @@ -238,6 +239,7 @@ dependencies { testCompile project(':groovy-ant') testCompile project(':groovy-test') + testCompile project(':groovy-macro') } ext.generatedDirectory = "${buildDir}/generated-sources" http://git-wip-us.apache.org/repos/asf/groovy/blob/cff7a3c3/src/spec/doc/core-metaprogramming.adoc ---------------------------------------------------------------------- diff --git a/src/spec/doc/core-metaprogramming.adoc b/src/spec/doc/core-metaprogramming.adoc index c76ae30..52344fd 100644 --- a/src/spec/doc/core-metaprogramming.adoc +++ b/src/spec/doc/core-metaprogramming.adoc @@ -30,7 +30,7 @@ With runtime metaprogramming we can postpone to runtime the decision to intercep In Groovy we work with three kinds of objects: POJO, POGO and Groovy Interceptors. Groovy allows metaprogramming for all types of objects but in different manner. - POJO - A regular Java object, whose class can be written in Java or any other language for the JVM. -- POGO - A Groovy object, whose class is written in Groovy. It extends `java.lang.Object` and implements the gapi:groovy.lang.GroovyObject[] interface by default. +- POGO - A Groovy object, whose class is written in Groovy. It extends `java.lang.Object` and implements the gapi:groovy.lang.GroovyObject[] interface by default. - Groovy Interceptor - A Groovy object that implements the gapi:groovy.lang.GroovyInterceptable[] interface and has method-interception capability, which we'll discuss in the <<core-metaprogramming.adoc#_groovyinterceptable,GroovyInterceptable>> section. For every method call Groovy checks whether the object is a POJO or a POGO. For POJOs, Groovy fetches it's `MetaClass` from the gapi:groovy.lang.MetaClassRegistry[] and delegates method invocation to it. For POGOs, Groovy takes more steps, as illustrated in the following figure: @@ -79,7 +79,7 @@ Here is a simple example: ---- include::{projectdir}/src/spec/test/metaprogramming/GroovyObjectTest.groovy[tags=groovy_get_property,indent=0] ---- -<1> Forwards the request to the getter for all properties except `field3`. +<1> Forwards the request to the getter for all properties except `field3`. You can intercept write access to properties by overriding the `setProperty()` method: @@ -139,7 +139,7 @@ something like this: class GORM { def dynamicMethods = [...] // an array of dynamic methods that use regex - + def methodMissing(String name, args) { def method = dynamicMethods.find { it.match(name) } if(method) { @@ -2829,6 +2829,153 @@ to use the Groovy Console, in particular the AST browser tool, to gain knowledge resource for learning is the https://github.com/apache/groovy/tree/master/src/test/org/codehaus/groovy/ast/builder[AST Builder] test suite. +==== Macros + +===== Introduction + +Until version 2.5.0, when developing AST transformations, developers should have a deep knowledge about how the AST +(Abstract Syntax Tree) was built by the compiler in order to know how to add new expressions or statements during +compile time. + +Although the use of `org.codehaus.groovy.ast.tool.GeneralUtils` static methods could mitigate the burden of creating +expressions and statements, it's still a low-level way of writing those AST nodes directly. +We needed something to abstract us from writing the AST directly and that's exactly what Groovy macros were made for. +They allow you to directly add code during compilation, without having to translate the code you had in mind to the +`org.codehaus.groovy.ast.*` node related classes. + +===== Statements and expressions + +Let's see an example, lets create a local AST transformation: `@AddMessageMethod`. When applied to a given class it +will add a new method called `getMessage` to that class. The method will return "42". The annotation is pretty +straight forward: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/MacroStatementTest.groovy[tags=addmethodannotation,indent=0] +---- + +What would the AST transformation look like without the use of a macro ? Something like this: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/MacroStatementTest.groovy[tags=addmethodtransformationwithoutmacro,indent=0] +---- + +<1> Create a return statement +<2> Create a constant expression "42" +<3> Adding the code to the new method +<4> Adding the new method to the annotated class + +If you're not used to the AST API, that definitely doesn't look like the code you had in mind. Now look how the +previous code simplifies with the use of macros. + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/MacroStatementTest.groovy[tags=basicWithMacro,indent=0] +---- + +<1> Much simpler. You wanted to add a return statement that returned "42" and that's exactly what you can read inside +the `macro` utility method. Your plain code will be translated for you to a `org.codehaus.groovy.ast.stmt.ReturnStatement` +<2> Adding the return statement to the new method +<3> Adding the new code to the annotated class + +Although the `macro` method is used in this example to create a **statement** the `macro` method could also be used to create +**expressions** as well, it depends on which `macro` signature you use: + +- `macro(Closure)`: Create a given statement with the code inside the closure. +- `macro(Boolean,Closure)`: if **true** wrap expressions inside the closure inside an statement, if **false** then return +an expression +- `macro(CompilePhase, Closure)`: Create a given statement with the code inside the closure in a specific compile phase +- `macro(CompilePhase, Boolean, Closure)`: Create an statement or an expression (true == statement, false == expression) +in a specific compilation phase. + +NOTE: All these signatures can be found at `org.codehaus.groovy.macro.runtime.MacroGroovyMethods` + +Sometimes we could be only interested in creating a given expression, not the whole statement, in order to do that we +should use any of the `macro` invocations with a boolean parameter: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/MacroExpressionTest.groovy[tags=addgettwotransformation,indent=0] +---- + +<1> We're telling macro not to wrap the expression in a statement, we're only interested in the expression +<2> Assigning the expression +<3> Creating a `ReturnStatement` using a method from `GeneralUtils` and returning the expression +<4> Adding the code to the new method +<5> Adding the method to the class + +===== Variable substitution + +Macros are great but we can't create anything useful or reusable if our macros couldn't receive parameters or resolve +surrounding variables. + +In the following example we're creating an AST transformation `@MD5` that when applied to a given String field will +add a method returning the MD5 value of that field. + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/MacroVariableSubstitutionTest.groovy[tags=md5annotation,indent=0] +---- + +And the transformation: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/MacroVariableSubstitutionTest.groovy[tags=md5transformation,indent=0] +---- + +<1> We need a reference to a variable expression +<2> If using a class outside the standard packages we should add any needed imports or use the qualified name. When +using the qualified named of a given static method you need to make sure it's resolved in the proper compile phase. In +this particular case we're instructing the macro to resolve it at the SEMANTIC_ANALYSIS phase, which is the first compile phase +with type information. +<3> In order to substitute any `expression` inside the macro we need to use the `$v` method. `$v` receives a closure as an +argument, and the closure is only allowed to substitute expressions, meaning classes inheriting +`org.codehaus.groovy.ast.expr.Expression`. + +===== MacroClass + +As we mentioned earlier, the `macro` method is only capable of producing `statements` and `expressions`. But what if we +want to produce other types of nodes, such as a method, a field and so on? + +`org.codehaus.groovy.macro.transform.MacroClass` can be used to create **classes** (ClassNode instances) in our +transformations the same way we created statements and expressions with the `macro` method before. + +The next example is a local transformation `@Statistics`. When applied to a given class, it will add two methods +**getMethodCount()** and **getFieldCount()** which return how many methods and fields within the class respectively. Here +is the marker annotation. + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/MacroClassTest.groovy[tags=statisticsannotation,indent=0] +---- + +And the AST transformation: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/MacroClassTest.groovy[tags=statisticstransformation,indent=0] +---- + +<1> Creating a template class +<2> Adding template class methods to the annotated class +<3> Passing the reference class +<4> Extracting reference class method count value expression +<5> Extracting reference class field count value expression +<6> Building the **getMethodCount()** method using reference's method count value expression +<7> Building the **getFieldCount()** method using reference's field count value expression + +Basically we've created the **Statistics** class as a template to avoid writing low level AST API, then we +copied methods created in the template class to their final destination. + +NOTE: Types inside the `MacroClass` implementation should be resolved inside, that's why we had to write +`java.lang.Integer` instead of simply writing `Integer`. + +IMPORTANT: Notice that we're using `@CompileDynamic`. That's because the way we use `MacroClass` is like we +were actually implementing it. So if you were using `@CompileStatic` it will complain because an implementation of +an abstract class can't be another different class. + ==== Testing AST transformations ===== Separating source trees @@ -2868,6 +3015,90 @@ The difference is that when you use `assertScript`, the code in the `assertScrip unit test is executed*. That is to say that this time, the `Subject` class will be compiled with debugging active, and the breakpoint is going to be hit. +===== ASTMatcher + +Sometimes you may want to make assertions over AST nodes; perhaps to filter the nodes, or to make sure a given +transformation has built the expected AST node. + +**Filtering nodes** + +For instance if you would like to apply a given transformation only to a specific set of AST nodes, you could +use **ASTMatcher** to filter these nodes. The following example shows how to transform a given expression to +another. Using **ASTMatcher** it looks for a specific expression `1 + 1` and it transforms it to `3`. That's why +we called it the `@Joking` example. + +First we create the `@Joking` annotation that only can be applied to methods: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/ASTMatcherFilteringTest.groovy[tags=jokingannotation,indent=0] +---- + +Then the transformation, that only applies an instance of `org.codehaus.groovy.ast.ClassCodeExpressionTransformer` +to all the expressions within the method code block. + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/ASTMatcherFilteringTest.groovy[tags=jokingtransformation,indent=0] +---- +<1> Get the method's code statement and apply the expression transformer + +And this is when the **ASTMatcher** is used to apply the transformation only to those expressions matching +the expression `1 + 1`. + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/ASTMatcherFilteringTest.groovy[tags=jokingexpressiontransformer,indent=0] +---- +<1> Builds the expression used as reference pattern +<2> Checks the current expression evaluated matches the reference expression +<3> If it matches then replaces the current expression with the expression built with `macro` + +Then you could test the implementation as follows: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/ASTMatcherFilteringTest.groovy[tags=jokingexample,indent=0] +---- + +**Unit testing AST transforms** + +Normally we test AST transformations just checking that the final use of the transformation does what we expect. But +it would be great if we could have an easy way to check, for example, that the nodes the transformation adds are what +we expected from the beginning. + +The following transformation adds a new method `giveMeTwo` to an annotated class. + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/ASTMatcherTestingTest.groovy[tags=twiceasttransformation,indent=0] +---- +<1> Adding the method to the annotated class +<2> Building a binary expression. The binary expression uses the same variable expression in both +sides of the `+` token (check `varX` method at **org.codehaus.groovy.ast.tool.GeneralUtils**). +<3> Builds a new **ClassNode** with a method called `giveMeTwo` which returns the result of an expression +passed as parameter. + +Now instead of creating a test executing the transformation over a given sample code. I would like to check that +the construction of the binary expression is done properly: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/ASTMatcherTestingTest.groovy[tags=testexpression,indent=0] +---- +<1> Using ASTMatcher as a category +<2> Build a template node +<3> Apply some constraints to that template node +<4> Tells compiler that `a` is a placeholder. +<5> Asserts reference node and current node are equal + +Of course you can/should always check the actual execution: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/metaprogramming/ASTMatcherTestingTest.groovy[tags=executiontesting,indent=0] +---- + ===== ASTTest Last but not least, testing an AST transformation is also about testing the state of the AST *during compilation*. Groovy http://git-wip-us.apache.org/repos/asf/groovy/blob/cff7a3c3/src/spec/test/metaprogramming/ASTMatcherFilteringTest.groovy ---------------------------------------------------------------------- diff --git a/src/spec/test/metaprogramming/ASTMatcherFilteringTest.groovy b/src/spec/test/metaprogramming/ASTMatcherFilteringTest.groovy new file mode 100644 index 0000000..5c40e5e --- /dev/null +++ b/src/spec/test/metaprogramming/ASTMatcherFilteringTest.groovy @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package metaprogramming + +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.ClassCodeExpressionTransformer +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.macro.matcher.ASTMatcher +import org.codehaus.groovy.transform.AbstractASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformationClass + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +class ASTMatcherFilteringTest extends GroovyTestCase { + + void testFilteringNodes() { + assertScript ''' + // tag::jokingexample[] + package metaprogramming + + class Something { + @Joking + Integer getResult() { + return 1 + 1 + } + } + + assert new Something().result == 3 + // end::jokingexample[] + ''' + } +} + +// tag::jokingannotation[] +@Retention(RetentionPolicy.SOURCE) +@Target([ElementType.METHOD]) +@GroovyASTTransformationClass(["metaprogramming.JokingASTTransformation"]) +@interface Joking { } +// end::jokingannotation[] + +// tag::jokingtransformation[] +@CompileStatic +@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION) +class JokingASTTransformation extends AbstractASTTransformation { + @Override + void visit(ASTNode[] nodes, SourceUnit source) { + MethodNode methodNode = (MethodNode) nodes[1] + + methodNode + .getCode() + .visit(new ConvertOnePlusOneToThree(source)) // <1> + } +} +// end::jokingtransformation[] + +// tag::jokingexpressiontransformer[] +class ConvertOnePlusOneToThree extends ClassCodeExpressionTransformer { + SourceUnit sourceUnit + + ConvertOnePlusOneToThree(SourceUnit sourceUnit) { + this.sourceUnit = sourceUnit + } + + @Override + Expression transform(Expression exp) { + Expression ref = macro { 1 + 1 } // <1> + + if (ASTMatcher.matches(ref, exp)) { // <2> + return macro { 3 } // <3> + } + + return super.transform(exp) + } +} +// end::jokingexpressiontransformer[] http://git-wip-us.apache.org/repos/asf/groovy/blob/cff7a3c3/src/spec/test/metaprogramming/ASTMatcherTestingTest.groovy ---------------------------------------------------------------------- diff --git a/src/spec/test/metaprogramming/ASTMatcherTestingTest.groovy b/src/spec/test/metaprogramming/ASTMatcherTestingTest.groovy new file mode 100644 index 0000000..d86101f --- /dev/null +++ b/src/spec/test/metaprogramming/ASTMatcherTestingTest.groovy @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package metaprogramming + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.Parameter +import org.codehaus.groovy.ast.expr.BinaryExpression +import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.ast.stmt.BlockStatement +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.macro.matcher.ASTMatcher +import org.codehaus.groovy.macro.transform.MacroClass +import org.codehaus.groovy.transform.AbstractASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformationClass + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +import static org.codehaus.groovy.ast.ClassHelper.Integer_TYPE +import static org.codehaus.groovy.ast.tools.GeneralUtils.* + +class ASTMatcherTestingTest extends GroovyTestCase { + + // tag::testexpression[] + void testTestingSumExpression() { + use(ASTMatcher) { // <1> + TwiceASTTransformation sample = new TwiceASTTransformation() + Expression referenceNode = macro { + a + a // <2> + }.withConstraints { // <3> + placeholder 'a' // <4> + } + + assert sample + .sumExpression + .matches(referenceNode) // <5> + } + } + // end::testexpression[] + + // tag::executiontesting[] + void testASTBehavior() { + assertScript ''' + package metaprogramming + + @Twice + class AAA { + + } + + assert new AAA().giveMeTwo(1) == 2 + ''' + } + // end::executiontesting[] +} + +@Retention(RetentionPolicy.SOURCE) +@Target([ElementType.TYPE]) +@GroovyASTTransformationClass(["metaprogramming.TwiceASTTransformation"]) +@interface Twice { } + +// tag::twiceasttransformation[] +@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION) +class TwiceASTTransformation extends AbstractASTTransformation { + + static final String VAR_X = 'x' + + @Override + void visit(ASTNode[] nodes, SourceUnit source) { + ClassNode classNode = (ClassNode) nodes[1] + MethodNode giveMeTwo = getTemplateClass(sumExpression) + .getDeclaredMethods('giveMeTwo') + .first() + + classNode.addMethod(giveMeTwo) // <1> + } + + BinaryExpression getSumExpression() { // <2> + return macro { + $v{ varX(VAR_X) } + + $v{ varX(VAR_X) } + } + } + + ClassNode getTemplateClass(Expression expression) { // <3> + return new MacroClass() { + class Template { + java.lang.Integer giveMeTwo(java.lang.Integer x) { + return $v { expression } + } + } + } + } +} +// end::twiceasttransformation[] http://git-wip-us.apache.org/repos/asf/groovy/blob/cff7a3c3/src/spec/test/metaprogramming/MacroClassTest.groovy ---------------------------------------------------------------------- diff --git a/src/spec/test/metaprogramming/MacroClassTest.groovy b/src/spec/test/metaprogramming/MacroClassTest.groovy new file mode 100644 index 0000000..818ce43 --- /dev/null +++ b/src/spec/test/metaprogramming/MacroClassTest.groovy @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package metaprogramming + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.macro.transform.MacroClass +import org.codehaus.groovy.transform.AbstractASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformationClass + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +import static org.codehaus.groovy.ast.tools.GeneralUtils.constX + +class MacroClassTest extends GroovyTestCase { + + void testMacroClass() { + assertScript ''' + package metaprogramming + + @Statistics + class Person { + Integer age + String name + } + + def person = new Person(age: 12, name: 'john') + + assert person.methodCount == 0 + assert person.fieldCount == 2 + ''' + } +} + +// tag::statisticsannotation[] +@Retention(RetentionPolicy.SOURCE) +@Target([ElementType.TYPE]) +@GroovyASTTransformationClass(["metaprogramming.StatisticsASTTransformation"]) +@interface Statistics {} +// end::statisticsannotation[] + +// tag::statisticstransformation[] +@CompileStatic +@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION) +class StatisticsASTTransformation extends AbstractASTTransformation { + + @Override + void visit(ASTNode[] nodes, SourceUnit source) { + ClassNode classNode = (ClassNode) nodes[1] + ClassNode templateClass = buildTemplateClass(classNode) // <1> + + templateClass.methods.each { MethodNode node -> // <2> + classNode.addMethod(node) + } + } + + @CompileDynamic + ClassNode buildTemplateClass(ClassNode reference) { // <3> + def methodCount = constX(reference.methods.size()) // <4> + def fieldCount = constX(reference.fields.size()) // <5> + + return new MacroClass() { + class Statistics { + java.lang.Integer getMethodCount() { // <6> + return $v { methodCount } + } + + java.lang.Integer getFieldCount() { // <7> + return $v { fieldCount } + } + } + } + } +} +// end::statisticstransformation[] http://git-wip-us.apache.org/repos/asf/groovy/blob/cff7a3c3/src/spec/test/metaprogramming/MacroExpressionTest.groovy ---------------------------------------------------------------------- diff --git a/src/spec/test/metaprogramming/MacroExpressionTest.groovy b/src/spec/test/metaprogramming/MacroExpressionTest.groovy new file mode 100644 index 0000000..4f36ab1 --- /dev/null +++ b/src/spec/test/metaprogramming/MacroExpressionTest.groovy @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package metaprogramming + +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.ClassHelper +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.Parameter +import org.codehaus.groovy.ast.expr.BinaryExpression +import org.codehaus.groovy.ast.stmt.ReturnStatement +import org.codehaus.groovy.ast.tools.GeneralUtils +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.transform.AbstractASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformationClass + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +class MacroExpressionTest extends GroovyTestCase { + + void testCreateExpressions() { + assertScript ''' + // add::addgettwosample[] + package metaprogramming + + @AddGetTwo + class A { + + } + + assert new A().getTwo() == 2 + // tag::addgettwosample[] + ''' + } +} + +// tag::addgettwoannotation[] +@Retention(RetentionPolicy.SOURCE) +@Target([ElementType.TYPE]) +@GroovyASTTransformationClass(["metaprogramming.AddGetTwoASTTransformation"]) +@interface AddGetTwo { } +// end::addgettwoannotation[] + +// tag::addgettwotransformation[] +@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION) +class AddGetTwoASTTransformation extends AbstractASTTransformation { + + BinaryExpression onePlusOne() { + return macro(false) { 1 + 1 } // <1> + } + + @Override + void visit(ASTNode[] nodes, SourceUnit source) { + ClassNode classNode = nodes[1] + BinaryExpression expression = onePlusOne() // <2> + ReturnStatement returnStatement = GeneralUtils.returnS(expression) // <3> + + MethodNode methodNode = + new MethodNode("getTwo", + ACC_PUBLIC, + ClassHelper.Integer_TYPE, + [] as Parameter[], + [] as ClassNode[], + returnStatement // <4> + ) + + classNode.addMethod(methodNode) // <5> + } +} +// end::addgettwotransformation[] http://git-wip-us.apache.org/repos/asf/groovy/blob/cff7a3c3/src/spec/test/metaprogramming/MacroStatementTest.groovy ---------------------------------------------------------------------- diff --git a/src/spec/test/metaprogramming/MacroStatementTest.groovy b/src/spec/test/metaprogramming/MacroStatementTest.groovy new file mode 100644 index 0000000..541341c --- /dev/null +++ b/src/spec/test/metaprogramming/MacroStatementTest.groovy @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package metaprogramming + +import org.codehaus.groovy.ast.* +import org.codehaus.groovy.ast.expr.ConstantExpression +import org.codehaus.groovy.ast.stmt.ReturnStatement +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.transform.AbstractASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformationClass + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +class MacroStatementTest extends GroovyTestCase { + + void testOlderASTTransformation() { + assertScript ''' + package metaprogramming + + @AddMethod + class A { + + } + + new A().message == '42' + ''' + } + + void testOlderASTTransformationWithMacros() { + assertScript ''' + package metaprogramming + + @AddMethodWithMacros + class A { + + } + + new A().message == '42' + ''' + } +} + +// tag::addmethodannotation[] +@Retention(RetentionPolicy.SOURCE) +@Target([ElementType.TYPE]) +@GroovyASTTransformationClass(["metaprogramming.AddMethodASTTransformation"]) +@interface AddMethod { } +// end::addmethodannotation[] + +// tag::addmethodtransformationwithoutmacro[] +@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION) +class AddMethodASTTransformation extends AbstractASTTransformation { + @Override + void visit(ASTNode[] nodes, SourceUnit source) { + ClassNode classNode = (ClassNode) nodes[1] + + ReturnStatement code = + new ReturnStatement( // <1> + new ConstantExpression("42")) // <2> + + MethodNode methodNode = + new MethodNode( + "getMessage", + ACC_PUBLIC, + ClassHelper.make(String), + [] as Parameter[], + [] as ClassNode[], + code) // <3> + + classNode.addMethod(methodNode) // <4> + } +} +// end::addmethodtransformationwithoutmacro[] + +@Retention(RetentionPolicy.SOURCE) +@Target([ElementType.TYPE]) +@GroovyASTTransformationClass(["metaprogramming.AddMethodWithMacrosASTTransformation"]) +@interface AddMethodWithMacros { } + +// tag::basicWithMacro[] +@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION) +class AddMethodWithMacrosASTTransformation extends AbstractASTTransformation { + @Override + void visit(ASTNode[] nodes, SourceUnit source) { + ClassNode classNode = (ClassNode) nodes[1] + + ReturnStatement simplestCode = macro { return "42" } // <1> + + MethodNode methodNode = + new MethodNode( + "getMessage", + ACC_PUBLIC, + ClassHelper.make(String), + [] as Parameter[], + [] as ClassNode[], + simplestCode) // <2> + + classNode.addMethod(methodNode) // <3> + } +} +// end::basicWithMacro[] http://git-wip-us.apache.org/repos/asf/groovy/blob/cff7a3c3/src/spec/test/metaprogramming/MacroVariableSubstitutionTest.groovy ---------------------------------------------------------------------- diff --git a/src/spec/test/metaprogramming/MacroVariableSubstitutionTest.groovy b/src/spec/test/metaprogramming/MacroVariableSubstitutionTest.groovy new file mode 100644 index 0000000..b5be446 --- /dev/null +++ b/src/spec/test/metaprogramming/MacroVariableSubstitutionTest.groovy @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package metaprogramming + +import org.codehaus.groovy.ast.* +import org.codehaus.groovy.ast.expr.VariableExpression +import org.codehaus.groovy.ast.stmt.BlockStatement +import org.codehaus.groovy.ast.tools.GeneralUtils +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.transform.AbstractASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformationClass + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +class MacroVariableSubstitutionTest extends GroovyTestCase { + + void testVariableSubstitution() { + assertScript ''' + package metaprogramming + + class A { + @MD5 + String word + } + + def instance = new A(word: "Groovy") + + assert instance.getWordMD5() == '2d19c57fdd4fdc270c971f69ee8d5169' + ''' + } +} +// tag::md5annotation[] +@Retention(RetentionPolicy.SOURCE) +@Target([ElementType.FIELD]) +@GroovyASTTransformationClass(["metaprogramming.MD5ASTTransformation"]) +@interface MD5 { } +// end::md5annotation[] + +// tag::md5transformation[] +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +class MD5ASTTransformation extends AbstractASTTransformation { + + @Override + void visit(ASTNode[] nodes, SourceUnit source) { + FieldNode fieldNode = nodes[1] + ClassNode classNode = fieldNode.declaringClass + String capitalizedName = fieldNode.name.capitalize() + MethodNode methodNode = new MethodNode( + "get${capitalizedName}MD5", + ACC_PUBLIC, + ClassHelper.STRING_TYPE, + [] as Parameter[], + [] as ClassNode[], + buildMD5MethodCode(fieldNode)) + + classNode.addMethod(methodNode) + } + + BlockStatement buildMD5MethodCode(FieldNode fieldNode) { + VariableExpression fieldVar = GeneralUtils.varX(fieldNode.name) // <1> + + return macro(CompilePhase.SEMANTIC_ANALYSIS, true) { // <2> + return java.security.MessageDigest + .getInstance('MD5') + .digest($v { fieldVar }.getBytes()) // <3> + .encodeHex() + .toString() + } + } +} +// end::md5transformation[]