From commits-return-5580-archive-asf-public=cust-asf.ponee.io@groovy.apache.org Wed Feb 21 09:20:06 2018 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id 72DE618061A for ; Wed, 21 Feb 2018 09:20:05 +0100 (CET) Received: (qmail 29785 invoked by uid 500); 21 Feb 2018 08:20:04 -0000 Mailing-List: contact commits-help@groovy.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@groovy.apache.org Delivered-To: mailing list commits@groovy.apache.org Received: (qmail 29774 invoked by uid 99); 21 Feb 2018 08:20:04 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 21 Feb 2018 08:20:04 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 55200E0014; Wed, 21 Feb 2018 08:20:04 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: paulk@apache.org To: commits@groovy.apache.org Message-Id: X-Mailer: ASF-Git Admin Mailer Subject: groovy git commit: GROOVY-7956: Provide an AST transformation which improves named parameter support (doco and support changing visibility) Date: Wed, 21 Feb 2018 08:20:04 +0000 (UTC) Repository: groovy Updated Branches: refs/heads/master 965bd6ee3 -> 0110400db GROOVY-7956: Provide an AST transformation which improves named parameter support (doco and support changing visibility) Project: http://git-wip-us.apache.org/repos/asf/groovy/repo Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/0110400d Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/0110400d Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/0110400d Branch: refs/heads/master Commit: 0110400dbb637e19755c39e21a3cfd74e777b872 Parents: 965bd6e Author: paulk Authored: Wed Feb 21 18:18:17 2018 +1000 Committer: paulk Committed: Wed Feb 21 18:19:51 2018 +1000 ---------------------------------------------------------------------- .../groovy/groovy/transform/NamedVariant.java | 63 +++++++++++ .../groovy/transform/VisibilityOptions.java | 43 ++++++++ .../groovy/transform/options/Visibility.java | 27 +++++ .../groovy/ast/tools/VisibilityUtils.java | 110 +++++++++++++++++++ .../NamedVariantASTTransformation.java | 6 +- .../transform/NamedVariantTransformTest.groovy | 32 +++--- 6 files changed, 265 insertions(+), 16 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/groovy/blob/0110400d/src/main/groovy/groovy/transform/NamedVariant.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/NamedVariant.java b/src/main/groovy/groovy/transform/NamedVariant.java index 8db0529..2a10470 100644 --- a/src/main/groovy/groovy/transform/NamedVariant.java +++ b/src/main/groovy/groovy/transform/NamedVariant.java @@ -26,9 +26,72 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Allows construction of a named-arg equivalent method or constructor. + * The method or constructor will have at least a first argument of type + * {@code Map} and may have more arguments. As such, it can be called + * using Groovy's named-arg syntax. The original method/constructor is retained + * and is called by the generated method/constructor. + * + * One benefit of this approach is the potential for improved type checking. + * The annotated "tuple" method/constructor can be type rich and will be checked + * as such during normal compilation. The generated method/constructor using + * the map argument will be named-argument friendly but the map also hides + * type information. The generated method however contains no business logic + * so the chance of errors is minimal. + * + * Any arguments identified as named arguments will be supplied as + * part of the map. Any additional arguments are supplied in the normal + * tuple style. + * + * Named arguments are identified in one of three ways: + *
    + *
  1. Use one or more {@code @NamedParam} annotations to explicitly identify such arguments
  2. + *
  3. Use one or more {@code @NamedDelegate} annotations to explicitly identify such arguments as + * delegate arguments
  4. + *
  5. If no arguments with {@code @NamedParam} or {@code @NamedDelegate} annotations are found the + * first argument is assumed to be an implicit named delegate
  6. + *
+ * Named arguments will be supplied via the map with their property name (configurable via + * annotation attributes within {@code @NamedParam}) being the key and value being the argument value. + * For named delegates, any properties of the delegate can become map keys. Duplicate keys across + * delegates or named parameters are not allowed. Delegate arguments must be + * compatible with Groovy's {@code as} cast operation from a {@code Map}. + * + * Here is an example using the implicit delegate approach. + *
+ * import groovy.transform.*
+ *
+ * {@code @ToString(includeNames=true, includeFields=true)}
+ * class Color {
+ *     Integer r, g, b
+ * }
+ *
+ * {@code @NamedVariant}
+ * String foo(Color shade) {
+ *     shade
+ * }
+ *
+ * def result = foo(g: 12, b: 42, r: 12)
+ * assert result.toString() == 'Color(r:12, g:12, b:42)'
+ * 
+ * The generated method will be something like this: + *
+ * String foo(Map args) {
+ *     return foo(args as Color)
+ * }
+ * 
+ * The generated method/constructor retains the visibility and return type of the original + * but the {@code @VisibilityOptions} annotation can be added to the visibility. You could have the + * annotated method/constructor private for instance but have the generated one be public. + */ @Incubating @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @GroovyASTTransformationClass("org.codehaus.groovy.transform.NamedVariantASTTransformation") public @interface NamedVariant { + /** + * If specified, must match the "id" attribute in the VisibilityOptions annotation. + */ + String visibilityId() default Undefined.STRING; } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/groovy/blob/0110400d/src/main/groovy/groovy/transform/VisibilityOptions.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/VisibilityOptions.java b/src/main/groovy/groovy/transform/VisibilityOptions.java new file mode 100644 index 0000000..be1f112 --- /dev/null +++ b/src/main/groovy/groovy/transform/VisibilityOptions.java @@ -0,0 +1,43 @@ +/* + * 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 groovy.transform.options.Visibility; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation used in the context of AST transformations to provide a custom visibility. + * + * @since 2.5 + */ +@java.lang.annotation.Documented +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD}) +public @interface VisibilityOptions { + Visibility value() default Visibility.UNDEFINED; + String id() default Undefined.STRING; + Visibility type() default Visibility.UNDEFINED; + Visibility method() default Visibility.UNDEFINED; + Visibility constructor() default Visibility.UNDEFINED; +// Visibility field() default Visibility.UNDEFINED; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/0110400d/src/main/groovy/groovy/transform/options/Visibility.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/options/Visibility.java b/src/main/groovy/groovy/transform/options/Visibility.java new file mode 100644 index 0000000..e0ea899 --- /dev/null +++ b/src/main/groovy/groovy/transform/options/Visibility.java @@ -0,0 +1,27 @@ +/* + * 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.options; + +public enum Visibility { + PUBLIC, + PROTECTED, + PACKAGE_PRIVATE, + PRIVATE, + UNDEFINED +} http://git-wip-us.apache.org/repos/asf/groovy/blob/0110400d/src/main/java/org/apache/groovy/ast/tools/VisibilityUtils.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/groovy/ast/tools/VisibilityUtils.java b/src/main/java/org/apache/groovy/ast/tools/VisibilityUtils.java new file mode 100644 index 0000000..46fefad --- /dev/null +++ b/src/main/java/org/apache/groovy/ast/tools/VisibilityUtils.java @@ -0,0 +1,110 @@ +/* + * 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.apache.groovy.ast.tools; + +import groovy.transform.VisibilityOptions; +import groovy.transform.options.Visibility; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.expr.ClassExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.PropertyExpression; + +import java.util.List; +import java.util.Map; + +import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching; +import static org.codehaus.groovy.transform.AbstractASTTransformation.getMemberStringValue; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_PROTECTED; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; + +public class VisibilityUtils { + private static final ClassNode VISIBILITY_OPTIONS_TYPE = makeWithoutCaching(VisibilityOptions.class, false); + + private VisibilityUtils() { + } + + public static int getVisibility(AnnotationNode anno, AnnotatedNode node, int originalModifiers) { + + List annotations = node.getAnnotations(VISIBILITY_OPTIONS_TYPE); + if (annotations.isEmpty()) return originalModifiers; + + String visId = getMemberStringValue(anno, "visibilityId", null); + + Visibility vis = null; + if (visId == null) { + vis = getVisForAnnotation(node, annotations.get(0), null); + } else { + for (AnnotationNode visAnno : annotations) { + vis = getVisForAnnotation(node, visAnno, visId); + if (vis != Visibility.UNDEFINED) break; + } + } + if (vis == null || vis == Visibility.UNDEFINED) return originalModifiers; + + int result = originalModifiers & ~(ACC_PUBLIC | ACC_PROTECTED | ACC_PRIVATE); + switch (vis) { + case PUBLIC: + result |= ACC_PUBLIC; + break; + case PROTECTED: + result |= ACC_PROTECTED; + break; + case PRIVATE: + result |= ACC_PRIVATE; + break; + + } + return result; + } + + private static Visibility getVisForAnnotation(AnnotatedNode node, AnnotationNode visAnno, String visId) { + Map visMembers = visAnno.getMembers(); + if (visMembers == null) return Visibility.UNDEFINED; + String id = getMemberStringValue(visAnno, "id", null); + if ((id == null && visId != null) || (id != null && !id.equals(visId))) return Visibility.UNDEFINED; + + Visibility vis = null; + if (node instanceof ConstructorNode) { + vis = getVisibility(visMembers.get("constructor")); + } else if (node instanceof MethodNode) { + vis = getVisibility(visMembers.get("method")); + } else if (node instanceof ClassNode) { + vis = getVisibility(visMembers.get("type")); + } + if (vis == null || vis == Visibility.UNDEFINED) { + vis = getVisibility(visMembers.get("value")); + } + return vis; + } + + private static Visibility getVisibility(Expression e) { + if (e instanceof PropertyExpression) { + PropertyExpression pe = (PropertyExpression) e; + if (pe.getObjectExpression() instanceof ClassExpression && pe.getObjectExpression().getText().equals("groovy.transform.options.Visibility")) { + return Visibility.valueOf(pe.getPropertyAsString()); + } + } + return Visibility.UNDEFINED; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/0110400d/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java index 3bd0e8f..70053bd 100644 --- a/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java @@ -48,6 +48,7 @@ import java.util.Map; import java.util.Set; import static org.apache.groovy.ast.tools.ClassNodeUtils.isInnerClass; +import static org.apache.groovy.ast.tools.VisibilityUtils.getVisibility; import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE; import static org.codehaus.groovy.ast.ClassHelper.make; import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching; @@ -162,11 +163,12 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation { } final BlockStatement body = new BlockStatement(); + int modifiers = getVisibility(anno, mNode, mNode.getModifiers()); if (mNode instanceof ConstructorNode) { body.addStatement(stmt(ctorX(ClassNode.THIS, args))); body.addStatement(inner); cNode.addConstructor( - mNode.getModifiers(), + modifiers, genParamsArray, mNode.getExceptions(), body @@ -176,7 +178,7 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation { body.addStatement(stmt(callThisX(mNode.getName(), args))); cNode.addMethod( mNode.getName(), - mNode.getModifiers(), + modifiers, mNode.getReturnType(), genParamsArray, mNode.getExceptions(), http://git-wip-us.apache.org/repos/asf/groovy/blob/0110400d/src/test/org/codehaus/groovy/transform/NamedVariantTransformTest.groovy ---------------------------------------------------------------------- diff --git a/src/test/org/codehaus/groovy/transform/NamedVariantTransformTest.groovy b/src/test/org/codehaus/groovy/transform/NamedVariantTransformTest.groovy index 6f0bea4..db15bab 100644 --- a/src/test/org/codehaus/groovy/transform/NamedVariantTransformTest.groovy +++ b/src/test/org/codehaus/groovy/transform/NamedVariantTransformTest.groovy @@ -49,41 +49,45 @@ class NamedVariantTransformTest extends GroovyShellTestCase { ''' } - void testNamedDelegate() { + void testNamedParamConstructor() { assertScript """ import groovy.transform.* @ToString(includeNames=true, includeFields=true) class Color { - Integer r, g, b - } - - @NamedVariant - String foo(Color shade) { - shade + @NamedVariant + Color(@NamedParam int r, @NamedParam int g, @NamedParam int b) { + this.r = r + this.g = g + this.b = b + } + private int r, g, b } - def result = foo(g: 12, b: 42, r: 12) - assert result == 'Color(r:12, g:12, b:42)' + assert new Color(r: 10, g: 20, b: 30).toString() == 'Color(r:10, g:20, b:30)' """ } - void testNamedParamConstructor() { + void testNamedParamConstructorVisibility() { assertScript """ import groovy.transform.* + import static groovy.transform.options.Visibility.* - @ToString(includeNames=true, includeFields=true) class Color { + private int r, g, b + + @VisibilityOptions(PUBLIC) @NamedVariant - Color(@NamedParam int r, @NamedParam int g, @NamedParam int b) { + private Color(@NamedParam int r, @NamedParam int g, @NamedParam int b) { this.r = r this.g = g this.b = b } - private int r, g, b } - assert new Color(r: 10, g: 20, b: 30).toString() == 'Color(r:10, g:20, b:30)' + def pubCons = Color.constructors + assert pubCons.size() == 1 + assert pubCons[0].parameterTypes[0] == Map """ }