You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2016/10/14 10:54:15 UTC
groovy git commit: Add groovy-macro documentation (closes #439)
Repository: groovy
Updated Branches:
refs/heads/master 9e1a65e99 -> cff7a3c3d
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/master
Commit: cff7a3c3d212f6d64058a427d34170bd30777fa2
Parents: 9e1a65e
Author: Mario Garcia <ma...@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[]