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/06/25 06:16:31 UTC

[1/2] groovy git commit: GROOVY-7860: Groovy could implement an @AutoImplement transform (closes #348)

Repository: groovy
Updated Branches:
  refs/heads/master 9d94e2e4f -> 257619e7a


GROOVY-7860: Groovy could implement an @AutoImplement transform (closes #348)


Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/b79f43b5
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/b79f43b5
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/b79f43b5

Branch: refs/heads/master
Commit: b79f43b54481f7bbff83fe110c28801395525c49
Parents: 9d94e2e
Author: paulk <pa...@asert.com.au>
Authored: Sun Jun 12 21:23:29 2016 +1000
Committer: paulk <pa...@asert.com.au>
Committed: Sat Jun 25 16:16:06 2016 +1000

----------------------------------------------------------------------
 src/main/groovy/transform/AutoImplement.java    | 128 +++++++++++++
 src/main/groovy/transform/Undefined.java        |   4 +
 .../groovy/antlr/AntlrParserPlugin.java         |  46 +++--
 .../AutoImplementASTTransformation.java         | 182 +++++++++++++++++++
 .../transform/AutoImplementTransformTest.groovy | 101 ++++++++++
 5 files changed, 444 insertions(+), 17 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/groovy/blob/b79f43b5/src/main/groovy/transform/AutoImplement.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/transform/AutoImplement.java b/src/main/groovy/transform/AutoImplement.java
new file mode 100644
index 0000000..c41b887
--- /dev/null
+++ b/src/main/groovy/transform/AutoImplement.java
@@ -0,0 +1,128 @@
+/*
+ * 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 groovy.transform;
+
+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 annotation used to provide default dummy methods for a class extending an abstract super class or
+ * implementing one or more interfaces.
+ * <p>
+ * Example usage:
+ * <pre>
+ * import groovy.transform.AutoImplement
+ *
+ * {@code @AutoImplement}
+ * class EmptyStringIterator implements Iterator<String> {
+ *     boolean hasNext() { false }
+ * }
+ *
+ * assert !new EmptyStringIterator().hasNext()
+ * </pre>
+ * In the above example, since {@code hasNext} returns false, the {@code next} method
+ * should never be called, so any dummy implementation would do for {@code next}.
+ * The "empty" implementation provided by default when using {@code @AutoImplement}
+ * will suffice - which effectively returns {@code null} in Groovy for non-void,
+ * non-primitive methods.
+ *
+ * As a point of interest, the default implementation for methods returning primitive
+ * types is to return the default value (which incidentally never satisfies Groovy truth).
+ * For {@code boolean} this means returning {@code false}, so for the above example we
+ * could have (albeit perhaps less instructive of our intent) by just using:
+ * <pre>
+ * {@code @AutoImplement}
+ * class EmptyStringIterator implements Iterator<String> { }
+ * </pre>
+ * If we didn't want to assume that callers of our {@code EmptyStringIterator} correctly followed
+ * the {@code Iterator} contract, then we might want to guard against inappropriate calls to {@code next}.
+ * Rather than just returning {@code null}, we might want to throw an exception. This is easily done using
+ * the {@code exception} annotation attribute as shown below:
+ * <pre>
+ * import groovy.transform.AutoImplement
+ * import static groovy.test.GroovyAssert.shouldFail
+ *
+ * {@code @AutoImplement}(exception=UnsupportedOperationException)
+ * class EmptyStringIterator implements Iterator<String> {
+ *     boolean hasNext() { false }
+ * }
+ *
+ * shouldFail(UnsupportedOperationException) {
+ *     new EmptyStringIterator().next()
+ * }
+ * </pre>
+ * All implemented methods will throw an instance of this exception constructed using its no-arg constructor.
+ *
+ * You can also supply a single {@code message} annotation attribute in which case the message will be passed
+ * as an argument during exception construction as shown in the following example:
+ * <pre>
+ * {@code @AutoImplement}(exception=UnsupportedOperationException, message='Not supported for this empty iterator')
+ * class EmptyStringIterator implements Iterator<String> {
+ *     boolean hasNext() { false }
+ * }
+ *
+ * def ex = shouldFail(UnsupportedOperationException) {
+ *     new EmptyStringIterator().next()
+ * }
+ * assert ex.message == 'Not supported for this empty iterator'
+ * </pre>
+ * Finally, you can alternatively supply a {@code code} annotation attribute in which case a closure
+ * block can be supplied which should contain the code to execute for all implemented methods. This can be
+ * seen in the following example:
+ * <pre>
+ * {@code @AutoImplement}(code = { throw new UnsupportedOperationException("Not supported for ${getClass().simpleName}") })
+ * class EmptyStringIterator implements Iterator<String> {
+ *     boolean hasNext() { false }
+ * }
+ *
+ * def ex = shouldFail(UnsupportedOperationException) {
+ *     new EmptyStringIterator().next()
+ * }
+ * assert ex.message == 'Not supported for EmptyStringIterator'
+ * </pre>
+ *
+ * @since 2.5.0
+ */
+@java.lang.annotation.Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.AutoImplementASTTransformation")
+public @interface AutoImplement {
+    /**
+     * If defined, all unimplemented methods will throw this exception.
+     * Will be ignored if {@code code} is defined.
+     */
+    Class<? extends RuntimeException> exception() default Undefined.UNDEFINED_EXCEPTION.class;
+
+    /**
+     * If {@code exception} is defined, {@code message} can be used to specify the exception message.
+     * Will be ignored if {@code code} is defined or {@code exception} isn't defined.
+     */
+    String message() default Undefined.STRING;
+
+    /**
+     * If defined, all unimplemented methods will execute the code found within the supplied closure.
+     */
+    Class code() default Undefined.CLASS.class;
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/b79f43b5/src/main/groovy/transform/Undefined.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/transform/Undefined.java b/src/main/groovy/transform/Undefined.java
index e8f7873..e94424c 100644
--- a/src/main/groovy/transform/Undefined.java
+++ b/src/main/groovy/transform/Undefined.java
@@ -28,6 +28,10 @@ public final class Undefined {
     private Undefined() {}
     public static final String STRING = "<DummyUndefinedMarkerString-DoNotUse>";
     public static final class CLASS {}
+    public static final class UNDEFINED_EXCEPTION extends RuntimeException {
+        private static final long serialVersionUID = -3960500360386581172L;
+    }
     public static boolean isUndefined(String other) { return STRING.equals(other); }
     public static boolean isUndefined(ClassNode other) { return CLASS.class.getName().equals(other.getName()); }
+    public static boolean isUndefinedException(ClassNode other) { return UNDEFINED_EXCEPTION.class.getName().equals(other.getName()); }
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/b79f43b5/src/main/org/codehaus/groovy/antlr/AntlrParserPlugin.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/antlr/AntlrParserPlugin.java b/src/main/org/codehaus/groovy/antlr/AntlrParserPlugin.java
index 55a80ac..28a0395 100644
--- a/src/main/org/codehaus/groovy/antlr/AntlrParserPlugin.java
+++ b/src/main/org/codehaus/groovy/antlr/AntlrParserPlugin.java
@@ -1009,23 +1009,7 @@ public class AntlrParserPlugin extends ASTHelper implements ParserPlugin, Groovy
         }
 
         if (classNode.isInterface() && initialValue == null && type != null) {
-            if (type == ClassHelper.int_TYPE) {
-                initialValue = new ConstantExpression(0);
-            } else if (type == ClassHelper.long_TYPE) {
-                initialValue = new ConstantExpression(0L);
-            } else if (type == ClassHelper.double_TYPE) {
-                initialValue = new ConstantExpression(0.0);
-            } else if (type == ClassHelper.float_TYPE) {
-                initialValue = new ConstantExpression(0.0F);
-            } else if (type == ClassHelper.boolean_TYPE) {
-                initialValue = ConstantExpression.FALSE;
-            } else if (type == ClassHelper.short_TYPE) {
-                initialValue = new ConstantExpression((short) 0);
-            } else if (type == ClassHelper.byte_TYPE) {
-                initialValue = new ConstantExpression((byte) 0);
-            } else if (type == ClassHelper.char_TYPE) {
-                initialValue = new ConstantExpression((char) 0);
-            }
+            initialValue = getDefaultValueForPrimitive(type);
         }
 
 
@@ -1077,6 +1061,34 @@ public class AntlrParserPlugin extends ASTHelper implements ParserPlugin, Groovy
         }
     }
 
+    public static Expression getDefaultValueForPrimitive(ClassNode type) {
+        if (type == ClassHelper.int_TYPE) {
+            return new ConstantExpression(0);
+        }
+        if (type == ClassHelper.long_TYPE) {
+            return new ConstantExpression(0L);
+        }
+        if (type == ClassHelper.double_TYPE) {
+            return new ConstantExpression(0.0);
+        }
+        if (type == ClassHelper.float_TYPE) {
+            return new ConstantExpression(0.0F);
+        }
+        if (type == ClassHelper.boolean_TYPE) {
+            return ConstantExpression.FALSE;
+        }
+        if (type == ClassHelper.short_TYPE) {
+            return new ConstantExpression((short) 0);
+        }
+        if (type == ClassHelper.byte_TYPE) {
+            return new ConstantExpression((byte) 0);
+        }
+        if (type == ClassHelper.char_TYPE) {
+            return new ConstantExpression((char) 0);
+        }
+        return null;
+    }
+
     protected ClassNode[] interfaces(AST node) {
         List<ClassNode> interfaceList = new ArrayList<ClassNode>();
         for (AST implementNode = node.getFirstChild(); implementNode != null; implementNode = implementNode.getNextSibling()) {

http://git-wip-us.apache.org/repos/asf/groovy/blob/b79f43b5/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
new file mode 100644
index 0000000..b098d44
--- /dev/null
+++ b/src/main/org/codehaus/groovy/transform/AutoImplementASTTransformation.java
@@ -0,0 +1,182 @@
+/*
+ * 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 org.codehaus.groovy.transform;
+
+import groovy.transform.AutoImplement;
+import groovy.transform.Undefined;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotatedNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+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.ClosureExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+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.SourceUnit;
+import org.objectweb.asm.Opcodes;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.codehaus.groovy.antlr.AntlrParserPlugin.getDefaultValueForPrimitive;
+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.returnS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
+import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpec;
+import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpecRecurse;
+import static org.codehaus.groovy.ast.tools.GenericsUtils.createGenericsSpec;
+
+/**
+ * Handles generation of code for the @AutoImplement annotation.
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class AutoImplementASTTransformation extends AbstractASTTransformation {
+    static final Class MY_CLASS = AutoImplement.class;
+    static final ClassNode MY_TYPE = make(MY_CLASS);
+    static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+
+    public void visit(ASTNode[] nodes, SourceUnit source) {
+        init(nodes, source);
+        AnnotatedNode parent = (AnnotatedNode) nodes[1];
+        AnnotationNode anno = (AnnotationNode) nodes[0];
+        if (!MY_TYPE.equals(anno.getClassNode())) return;
+
+        if (parent instanceof ClassNode) {
+            ClassNode cNode = (ClassNode) parent;
+            if (!checkNotInterface(cNode, MY_TYPE_NAME)) return;
+            ClassNode exception = getMemberClassValue(anno, "exception");
+            if (exception != null && Undefined.isUndefinedException(exception)) {
+                exception = null;
+            }
+            String message = getMemberStringValue(anno, "message");
+            Expression code = anno.getMember("code");
+            if (code != null && !(code instanceof ClosureExpression)) {
+                addError("Expected closure value for annotation parameter 'code'. Found " + code, cNode);
+                return;
+            }
+            createMethods(cNode, exception, message, (ClosureExpression) code);
+            if (code != null) {
+                anno.setMember("code", new ClosureExpression(new Parameter[0], new EmptyStatement()));
+            }
+        }
+    }
+
+    private void createMethods(ClassNode cNode, ClassNode exception, String message, ClosureExpression code) {
+        for (MethodNode candidate : getAllCorrectedMethodsMap(cNode).values()) {
+            if (candidate.isAbstract()) {
+                cNode.addMethod(candidate.getName(), Opcodes.ACC_PUBLIC, candidate.getReturnType(),
+                        candidate.getParameters(), candidate.getExceptions(),
+                        methodBody(exception, message, code, candidate.getReturnType()));
+            }
+        }
+    }
+
+    /**
+     * Return all methods including abstract super/interface methods but only if not overridden
+     * by a concrete declared/inherited method.
+     */
+    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);
+        }
+        ClassNode next = cNode;
+        while (true) {
+            Map<String, ClassNode> genericsSpec = createGenericsSpec(next);
+            for (MethodNode mn : next.getMethods()) {
+                MethodNode correctedMethod = correctToGenericsSpec(genericsSpec, mn);
+                if (next != cNode) {
+                    ClassNode correctedClass = correctToGenericsSpecRecurse(genericsSpec, next);
+                    MethodNode found = getDeclaredMethodCorrected(genericsSpec, correctedMethod, correctedClass);
+                    if (found != null) {
+                        String td = found.getTypeDescriptor();
+                        if (result.containsKey(td) && !result.get(td).isAbstract()) {
+                            continue;
+                        }
+                        result.put(td, found);
+                    }
+                }
+            }
+            List<ClassNode> interfaces = new ArrayList<ClassNode>(Arrays.asList(next.getInterfaces()));
+            Map<String, ClassNode> updatedGenericsSpec = new HashMap<String, ClassNode>(genericsSpec);
+            while (!interfaces.isEmpty()) {
+                ClassNode origInterface = interfaces.remove(0);
+                if (!origInterface.equals(ClassHelper.OBJECT_TYPE)) {
+                    updatedGenericsSpec = createGenericsSpec(origInterface, updatedGenericsSpec);
+                    ClassNode correctedInterface = correctToGenericsSpecRecurse(updatedGenericsSpec, origInterface);
+                    for (MethodNode nextMethod : correctedInterface.getMethods()) {
+                        MethodNode correctedMethod = correctToGenericsSpec(genericsSpec, nextMethod);
+                        MethodNode found = getDeclaredMethodCorrected(updatedGenericsSpec, correctedMethod, correctedInterface);
+                        if (found != null) {
+                            String td = found.getTypeDescriptor();
+                            if (result.containsKey(td) && !result.get(td).isAbstract()) {
+                                continue;
+                            }
+                            result.put(td, found);
+                        }
+                    }
+                    interfaces.addAll(Arrays.asList(correctedInterface.getInterfaces()));
+                }
+            }
+            ClassNode superClass = next.getUnresolvedSuperClass();
+            if (superClass == null) {
+                break;
+            }
+            next = correctToGenericsSpecRecurse(updatedGenericsSpec, superClass);
+        }
+        return result;
+    }
+
+    private static MethodNode getDeclaredMethodCorrected(Map<String, ClassNode> genericsSpec, MethodNode origMethod, ClassNode correctedClass) {
+        for (MethodNode nameMatch : correctedClass.getDeclaredMethods(origMethod.getName())) {
+            MethodNode correctedMethod = correctToGenericsSpec(genericsSpec, nameMatch);
+            if (ParameterUtils.parametersEqual(correctedMethod.getParameters(), origMethod.getParameters())) {
+                return correctedMethod;
+            }
+        }
+        return null;
+    }
+
+    private BlockStatement methodBody(ClassNode exception, String message, ClosureExpression code, ClassNode returnType) {
+        BlockStatement body = new BlockStatement();
+        if (code != null) {
+            body.addStatement(code.getCode());
+        } else if (exception != null) {
+            body.addStatement(throwS(ctorX(exception, message == null ? EMPTY_ARGUMENTS : constX(message))));
+        } else {
+            Expression result = getDefaultValueForPrimitive(returnType);
+            if (result != null) {
+                body.addStatement(returnS(result));
+            }
+        }
+        return body;
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/b79f43b5/src/test/org/codehaus/groovy/transform/AutoImplementTransformTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/org/codehaus/groovy/transform/AutoImplementTransformTest.groovy b/src/test/org/codehaus/groovy/transform/AutoImplementTransformTest.groovy
new file mode 100644
index 0000000..26fdc6f
--- /dev/null
+++ b/src/test/org/codehaus/groovy/transform/AutoImplementTransformTest.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 org.codehaus.groovy.transform
+
+class AutoImplementTransformTest extends GroovyShellTestCase {
+
+    void testGenericReturnTypes() {
+        assertScript '''
+            interface HasXs<T> {
+                T[] x()
+            }
+
+            abstract class HasXsY<E> implements HasXs<Long> {
+                abstract E y()
+            }
+
+            interface MyIt<T> extends Iterator<T> {}
+
+            @groovy.transform.AutoImplement
+            class Foo extends HasXsY<Integer> implements MyIt<String> { }
+
+            def publicMethods = Foo.methods.findAll{ it.modifiers == 1 }.collect{ "$it.returnType.simpleName $it.name" }*.toString()
+            assert ['boolean hasNext', 'String next', 'Long[] x', 'Integer y'].every{ publicMethods.contains(it) }
+            '''
+    }
+
+    void testException() {
+        shouldFail UnsupportedOperationException, '''
+            import groovy.transform.*
+
+            @AutoImplement(exception=UnsupportedOperationException)
+            class Foo implements Iterator<String> { }
+
+            new Foo().hasNext()
+        '''
+    }
+
+    void testExceptionWithMessage() {
+        def message = shouldFail UnsupportedOperationException, '''
+            import groovy.transform.*
+
+            @AutoImplement(exception=UnsupportedOperationException, message='Not supported by Foo')
+            class Foo implements Iterator<String> { }
+
+            new Foo().hasNext()
+        '''
+        assert message.contains('Not supported by Foo')
+    }
+
+    void testClosureBody() {
+        shouldFail IllegalStateException, '''
+            import groovy.transform.*
+
+            @AutoImplement(code={ throw new IllegalStateException()})
+            class Foo implements Iterator<String> { }
+
+            new Foo().hasNext()
+        '''
+    }
+
+    void testInheritedMethodNotOverwritten() {
+        assertScript '''
+            class WithNext {
+                String next() { 'foo' }
+            }
+
+            @groovy.transform.AutoImplement
+            class Foo extends WithNext implements Iterator<String> { }
+            assert new Foo().next() == 'foo'
+        '''
+    }
+
+    void testExistingMethodNotOverwritten() {
+        assertScript '''
+            @groovy.transform.AutoImplement
+            class Foo implements Iterator<String> {
+                String next() { 'foo' }
+            }
+
+            assert new Foo().next() == 'foo'
+        '''
+    }
+
+}


[2/2] groovy git commit: GROOVY-7860: Groovy could implement an @AutoImplement transform (tweaks and additional doco)

Posted by pa...@apache.org.
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[]
+*/
+        '''
+    }
 }