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'
+        '''
+    }
+
+}

Reply via email to