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 <pa...@asert.com.au>
Authored: Wed Feb 21 18:18:17 2018 +1000
Committer: paulk <pa...@asert.com.au>
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:
+ * <ol>
+ *     <li>Use one or more {@code @NamedParam} annotations to explicitly 
identify such arguments</li>
+ *     <li>Use one or more {@code @NamedDelegate} annotations to explicitly 
identify such arguments as
+ *     delegate arguments</li>
+ *     <li>If no arguments with {@code @NamedParam} or {@code @NamedDelegate} 
annotations are found the
+ *     first argument is assumed to be an implicit named delegate</li>
+ * </ol>
+ * 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.
+ * <pre class="groovyTestCase">
+ * 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)'
+ * </pre>
+ * The generated method will be something like this:
+ * <pre>
+ * String foo(Map args) {
+ *     return foo(args as Color)
+ * }
+ * </pre>
+ * 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<AnnotationNode> 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<String, Expression> 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
         """
     }
 

Reply via email to