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