GROOVY-7860: Groovy could implement an @AutoImplement transform (tweaks and additional doco)
Project: http://git-wip-us.apache.org/repos/asf/groovy/repo Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/257619e7 Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/257619e7 Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/257619e7 Branch: refs/heads/master Commit: 257619e7a6e2edb9a3fbf9d9e71e0c12b6575c5e Parents: b79f43b Author: paulk <pa...@asert.com.au> Authored: Thu Jun 16 20:46:57 2016 +1000 Committer: paulk <pa...@asert.com.au> Committed: Sat Jun 25 16:16:08 2016 +1000 ---------------------------------------------------------------------- .../AutoImplementASTTransformation.java | 8 +- src/spec/doc/core-metaprogramming.adoc | 112 +++++++++++++++ .../test/CodeGenerationASTTransformsTest.groovy | 140 +++++++++++++++++++ 3 files changed, 257 insertions(+), 3 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/groovy/blob/257619e7/src/main/org/codehaus/groovy/transform/AutoImplementASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/org/codehaus/groovy/transform/AutoImplementASTTransformation.java b/src/main/org/codehaus/groovy/transform/AutoImplementASTTransformation.java index b098d44..6366627 100644 --- a/src/main/org/codehaus/groovy/transform/AutoImplementASTTransformation.java +++ b/src/main/org/codehaus/groovy/transform/AutoImplementASTTransformation.java @@ -34,6 +34,7 @@ import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.EmptyStatement; import org.codehaus.groovy.ast.tools.ParameterUtils; import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.GenericsVisitor; import org.codehaus.groovy.control.SourceUnit; import org.objectweb.asm.Opcodes; @@ -48,6 +49,7 @@ import static org.codehaus.groovy.ast.ClassHelper.make; import static org.codehaus.groovy.ast.expr.ArgumentListExpression.EMPTY_ARGUMENTS; import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.makeDescriptorWithoutReturnType; import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS; import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpec; @@ -106,7 +108,7 @@ public class AutoImplementASTTransformation extends AbstractASTTransformation { private static Map<String, MethodNode> getAllCorrectedMethodsMap(ClassNode cNode) { Map<String, MethodNode> result = new HashMap<String, MethodNode>(); for (MethodNode mn : cNode.getMethods()) { - result.put(mn.getTypeDescriptor(), mn); + result.put(makeDescriptorWithoutReturnType(mn), mn); } ClassNode next = cNode; while (true) { @@ -117,7 +119,7 @@ public class AutoImplementASTTransformation extends AbstractASTTransformation { ClassNode correctedClass = correctToGenericsSpecRecurse(genericsSpec, next); MethodNode found = getDeclaredMethodCorrected(genericsSpec, correctedMethod, correctedClass); if (found != null) { - String td = found.getTypeDescriptor(); + String td = makeDescriptorWithoutReturnType(found); if (result.containsKey(td) && !result.get(td).isAbstract()) { continue; } @@ -136,7 +138,7 @@ public class AutoImplementASTTransformation extends AbstractASTTransformation { MethodNode correctedMethod = correctToGenericsSpec(genericsSpec, nextMethod); MethodNode found = getDeclaredMethodCorrected(updatedGenericsSpec, correctedMethod, correctedInterface); if (found != null) { - String td = found.getTypeDescriptor(); + String td = makeDescriptorWithoutReturnType(found); if (result.containsKey(td) && !result.get(td).isAbstract()) { continue; } http://git-wip-us.apache.org/repos/asf/groovy/blob/257619e7/src/spec/doc/core-metaprogramming.adoc ---------------------------------------------------------------------- diff --git a/src/spec/doc/core-metaprogramming.adoc b/src/spec/doc/core-metaprogramming.adoc index 2e0e47b..c76ae30 100644 --- a/src/spec/doc/core-metaprogramming.adoc +++ b/src/spec/doc/core-metaprogramming.adoc @@ -1429,6 +1429,118 @@ documentation. The annotation attribute `forClass` is not supported for this strategy. +[[xform-AutoImplement]] +===== `@groovy.transform.AutoImplement` + +The `@AutoImplement` AST transformation supplies dummy implementations for any found abstract methods from +superclasses or interfaces. The dummy implementation is the same for all abstract methods found and can be: + +* essentially empty (exactly true for void methods and for methods with a return type, returns the default value for +that type) +* a statement that throws a specified exception (with optional message) +* some user supplied code + +The first example illustrates the default case. Our class is annotated with `@AutoImplement`, +has a superclass and a single interface as can be seen here: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_default,indent=0] +---- + +A `void close()` method from the +`Closeable` interface is supplied and left empty. Implementations are also supplied +for the three abstract methods from the super class. The `get`, `addAll` and `size` methods +have return types of `String`, `boolean` and `int` respectively with default values +`null`, `false` and `0`. We can use our class (and check the expected return type for one +of the methods) using the following code: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_default_usage,indent=0] +---- + +It is also worthwhile examining the equivalent generated code: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_default_equiv,indent=0] +---- + +The second example illustrates the simplest exception case. Our class is annotated with `@AutoImplement`, +has a superclass and an annotation attribute indicates that an `IOException` should be thrown if any of +our "dummy" methods are called. Here is the class definition: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_exception,indent=0] +---- + +We can use the class (and check the expected exception is thrown for one +of the methods) using the following code: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_exception_usage,indent=0] +---- + +It is also worthwhile examining the equivalent generated code where three void methods +have been provided all of which throw the supplied exception: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_exception_equiv,indent=0] +---- + +The third example illustrates the exception case with a supplied message. Our class is annotated with `@AutoImplement`, +implements an interface, and has annotation attributes to indicate that an `UnsupportedOperationException` with +`Not supported by MyIterator` as the message should be thrown for any supplied methods. Here is the class definition: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_exceptionmsg,indent=0] +---- + +We can use the class (and check the expected exception is thrown and has the correct message +for one of the methods) using the following code: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_exceptionmsg_usage,indent=0] +---- + +It is also worthwhile examining the equivalent generated code where three void methods +have been provided all of which throw the supplied exception: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_exceptionmsg_equiv,indent=0] +---- + +The fourth example illustrates the case of user supplied code. Our class is annotated with `@AutoImplement`, +implements an interface, has an explcitly overriden `hasNext` method, and has an annotation attribute containing the +supplied code for any supplied methods. Here is the class definition: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_code,indent=0] +---- + +We can use the class (and check the expected exception is thrown and has a message of the expected form) +using the following code: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_code_usage,indent=0] +---- + +It is also worthwhile examining the equivalent generated code where the `next` method has been supplied: + +[source,groovy] +---- +include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=autoimplement_code_equiv,indent=0] +---- + ==== Class design annotations This category of annotations are aimed at simplifying the implementation of well-known design patterns (delegation, http://git-wip-us.apache.org/repos/asf/groovy/blob/257619e7/src/spec/test/CodeGenerationASTTransformsTest.groovy ---------------------------------------------------------------------- diff --git a/src/spec/test/CodeGenerationASTTransformsTest.groovy b/src/spec/test/CodeGenerationASTTransformsTest.groovy index 0c70516..f6332ca 100644 --- a/src/spec/test/CodeGenerationASTTransformsTest.groovy +++ b/src/spec/test/CodeGenerationASTTransformsTest.groovy @@ -1395,4 +1395,144 @@ createFirstLastBorn() // end::builder_initializer_immutable[] ''' } + + void testAutoImplement() { + assertScript ''' +// tag::autoimplement_default[] +import groovy.transform.AutoImplement + +@AutoImplement +class MyNames extends AbstractList<String> implements Closeable { } +// end::autoimplement_default[] + +// tag::autoimplement_default_usage[] +assert new MyNames().size() == 0 +// end::autoimplement_default_usage[] + +/* +// tag::autoimplement_default_equiv[] +class MyNames implements Closeable extends AbstractList<String> { + + String get(int param0) { + return null + } + + boolean addAll(Collection<? extends String> param0) { + return false + } + + void close() throws Exception { + } + + int size() { + return 0 + } + +} +// end::autoimplement_default_equiv[] +*/ + ''' + + assertScript ''' +import groovy.transform.AutoImplement +// tag::autoimplement_exception[] +@AutoImplement(exception=IOException) +class MyWriter extends Writer { } +// end::autoimplement_exception[] + +// tag::autoimplement_exception_usage[] +import static groovy.test.GroovyAssert.shouldFail + +shouldFail(IOException) { + new MyWriter().flush() +} +// end::autoimplement_exception_usage[] + +/* +// tag::autoimplement_exception_equiv[] +class MyWriter extends Writer { + + void flush() throws IOException { + throw new IOException() + } + + void write(char[] param0, int param1, int param2) throws IOException { + throw new IOException() + } + + void close() throws Exception { + throw new IOException() + } + +} +// end::autoimplement_exception_equiv[] +*/ + ''' + + assertScript ''' +import groovy.transform.AutoImplement +// tag::autoimplement_exceptionmsg[] +@AutoImplement(exception=UnsupportedOperationException, message='Not supported by MyIterator') +class MyIterator implements Iterator<String> { } +// end::autoimplement_exceptionmsg[] + +import static groovy.test.GroovyAssert.shouldFail +// tag::autoimplement_exceptionmsg_usage[] +def ex = shouldFail(UnsupportedOperationException) { + new MyIterator().hasNext() +} +assert ex.message == 'Not supported by MyIterator' +// end::autoimplement_exceptionmsg_usage[] + +/* +// tag::autoimplement_exceptionmsg_equiv[] +class MyIterator implements Iterator<String> { + + boolean hasNext() { + throw new UnsupportedOperationException('Not supported by MyIterator') + } + + String next() { + throw new UnsupportedOperationException('Not supported by MyIterator') + } + +} +// end::autoimplement_exceptionmsg_equiv[] +*/ + ''' + + assertScript ''' +import groovy.transform.AutoImplement +// tag::autoimplement_code[] +@AutoImplement(code = { throw new UnsupportedOperationException('Should never be called but was called on ' + new Date()) }) +class EmptyIterator implements Iterator<String> { + boolean hasNext() { false } +} +// end::autoimplement_code[] + +import static groovy.test.GroovyAssert.shouldFail +// tag::autoimplement_code_usage[] +def ex = shouldFail(UnsupportedOperationException) { + new EmptyIterator().next() +} +assert ex.message.startsWith('Should never be called but was called on ') +// end::autoimplement_code_usage[] + +/* +// tag::autoimplement_code_equiv[] +class EmptyIterator implements java.util.Iterator<String> { + + boolean hasNext() { + false + } + + String next() { + throw new UnsupportedOperationException('Should never be called but was called on ' + new Date()) + } + +} +// end::autoimplement_code_equiv[] +*/ + ''' + } }