You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2020/08/10 23:34:45 UTC

[groovy] branch GROOVY_3_0_X updated: Support coerce for @NamedVariant

This is an automated email from the ASF dual-hosted git repository.

sunlan pushed a commit to branch GROOVY_3_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/GROOVY_3_0_X by this push:
     new ffd4c98  Support coerce for @NamedVariant
ffd4c98 is described below

commit ffd4c98826a0d706652ed696c2d72bb95193c863
Author: MoonFruit <dk...@gmail.com>
AuthorDate: Thu Aug 6 17:27:04 2020 +0800

    Support coerce for @NamedVariant
    
    (cherry picked from commit dfd38f36b900df66b9135b6658492e5ab16be2ee)
---
 src/main/java/groovy/transform/NamedVariant.java   |  5 ++
 .../codehaus/groovy/ast/tools/GeneralUtils.java    |  4 +
 .../transform/NamedVariantASTTransformation.java   | 32 +++++---
 src/test/groovy/transform/NamedVariantTest.groovy  | 86 ++++++++++++++++++++++
 4 files changed, 116 insertions(+), 11 deletions(-)

diff --git a/src/main/java/groovy/transform/NamedVariant.java b/src/main/java/groovy/transform/NamedVariant.java
index 04faeb5..8cd9abb 100644
--- a/src/main/java/groovy/transform/NamedVariant.java
+++ b/src/main/java/groovy/transform/NamedVariant.java
@@ -124,4 +124,9 @@ public @interface NamedVariant {
      * @since 2.5.3
      */
     boolean autoDelegate() default false;
+
+    /**
+     * If true, will use {@code as} to convert map parameter to required class
+     */
+    boolean coerce() default false;
 }
diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
index 833e40c..4d1c417 100644
--- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
+++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
@@ -119,6 +119,10 @@ public class GeneralUtils {
         return args(Arrays.stream(names).map(GeneralUtils::varX).toArray(Expression[]::new));
     }
 
+    public static CastExpression asX(final ClassNode type, final Expression expression) {
+        return CastExpression.asExpression(type, expression);
+    }
+
     public static Statement assignS(final Expression target, final Expression value) {
         return stmt(assignX(target, value));
     }
diff --git a/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java
index 419dada..6ce13c5 100644
--- a/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java
@@ -55,6 +55,7 @@ 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;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.asX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.boolX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
@@ -95,6 +96,7 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
         }
 
         boolean autoDelegate = memberHasValue(anno, "autoDelegate", true);
+        boolean coerce = memberHasValue(anno, "coerce", true);
         Parameter mapParam = param(GenericsUtils.nonGeneric(MAP_TYPE), "__namedArgs");
         List<Parameter> genParams = new ArrayList<>();
         genParams.add(mapParam);
@@ -113,17 +115,17 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
 
         if (!annoFound && autoDelegate) {
             // assume the first param is the delegate by default
-            processDelegateParam(mNode, mapParam, args, propNames, fromParams[0]);
+            processDelegateParam(mNode, mapParam, args, propNames, fromParams[0], coerce);
         } else {
             for (Parameter fromParam : fromParams) {
                 if (!annoFound) {
-                    if (!processImplicitNamedParam(mNode, mapParam, args, propNames, fromParam)) return;
+                    if (!processImplicitNamedParam(mNode, mapParam, args, propNames, fromParam, coerce)) return;
                 } else if (AnnotatedNodeUtils.hasAnnotation(fromParam, NAMED_PARAM_TYPE)) {
-                    if (!processExplicitNamedParam(mNode, mapParam, inner, args, propNames, fromParam)) return;
+                    if (!processExplicitNamedParam(mNode, mapParam, inner, args, propNames, fromParam, coerce)) return;
                 } else if (AnnotatedNodeUtils.hasAnnotation(fromParam, NAMED_DELEGATE_TYPE)) {
-                    if (!processDelegateParam(mNode, mapParam, args, propNames, fromParam)) return;
+                    if (!processDelegateParam(mNode, mapParam, args, propNames, fromParam, coerce)) return;
                 } else {
-                    args.addExpression(varX(fromParam));
+                    args.addExpression(asType(varX(fromParam), fromParam.getType(), coerce));
                     if (hasDuplicates(mNode, propNames, fromParam.getName())) return;
                     genParams.add(fromParam);
                 }
@@ -132,7 +134,7 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
         createMapVariant(mNode, anno, mapParam, genParams, cNode, inner, args, propNames);
     }
 
-    private boolean processImplicitNamedParam(final MethodNode mNode, final Parameter mapParam, final ArgumentListExpression args, final List<String> propNames, final Parameter fromParam) {
+    private boolean processImplicitNamedParam(final MethodNode mNode, final Parameter mapParam, final ArgumentListExpression args, final List<String> propNames, final Parameter fromParam, boolean coerce) {
         boolean required = fromParam.hasInitialExpression();
         String name = fromParam.getName();
         if (hasDuplicates(mNode, propNames, name)) return false;
@@ -141,11 +143,11 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
         namedParam.addMember("type", classX(fromParam.getType()));
         namedParam.addMember("required", constX(required, true));
         mapParam.addAnnotation(namedParam);
-        args.addExpression(propX(varX(mapParam), name));
+        args.addExpression(asType(propX(varX(mapParam), name), fromParam.getType(), coerce));
         return true;
     }
 
-    private boolean processExplicitNamedParam(final MethodNode mNode, final Parameter mapParam, final BlockStatement inner, final ArgumentListExpression args, final List<String> propNames, final Parameter fromParam) {
+    private boolean processExplicitNamedParam(final MethodNode mNode, final Parameter mapParam, final BlockStatement inner, final ArgumentListExpression args, final List<String> propNames, final Parameter fromParam, boolean coerce) {
         AnnotationNode namedParam = fromParam.getAnnotations(NAMED_PARAM_TYPE).get(0);
         boolean required = memberHasValue(namedParam, "required", true);
         if (getMemberStringValue(namedParam, "value") == null) {
@@ -166,13 +168,13 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
             inner.addStatement(new AssertStatement(boolX(callX(varX(mapParam), "containsKey", args(constX(name)))),
                     plusX(constX("Missing required named argument '" + name + "'. Keys found: "), callX(varX(mapParam), "keySet"))));
         }
-        args.addExpression(propX(varX(mapParam), name));
+        args.addExpression(asType(propX(varX(mapParam), name), fromParam.getType(), coerce));
         mapParam.addAnnotation(namedParam);
         fromParam.getAnnotations().remove(namedParam);
         return true;
     }
 
-    private boolean processDelegateParam(final MethodNode mNode, final Parameter mapParam, final ArgumentListExpression args, final List<String> propNames, final Parameter fromParam) {
+    private boolean processDelegateParam(final MethodNode mNode, final Parameter mapParam, final ArgumentListExpression args, final List<String> propNames, final Parameter fromParam, boolean coerce) {
         if (isInnerClass(fromParam.getType())) {
             if (mNode.isStatic()) {
                 addError("Error during " + NAMED_VARIANT + " processing. Delegate type '" + fromParam.getType().getNameWithoutPackage() + "' is an inner class which is not supported.", mNode);
@@ -190,7 +192,7 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
             String name = pNode.getName();
             // create entry [name: __namedArgs.getOrDefault('name', initialValue)]
             Expression defaultValue = Optional.ofNullable(pNode.getInitialExpression()).orElseGet(() -> getDefaultExpression(pNode.getType()));
-            entries.add(entryX(constX(name), callX(varX(mapParam), "getOrDefault", args(constX(name), defaultValue))));
+            entries.add(entryX(constX(name), asType(callX(varX(mapParam), "getOrDefault", args(constX(name), defaultValue)), pNode.getType(), coerce)));
             // create annotation @NamedParam(value='name', type=DelegateType)
             AnnotationNode namedParam = new AnnotationNode(NAMED_PARAM_TYPE);
             namedParam.addMember("value", constX(name));
@@ -257,4 +259,12 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
             );
         }
     }
+
+    private Expression asType(Expression expression, ClassNode classNode, boolean coerce) {
+        if (coerce) {
+            return asX(classNode, expression);
+        } else {
+            return expression;
+        }
+    }
 }
diff --git a/src/test/groovy/transform/NamedVariantTest.groovy b/src/test/groovy/transform/NamedVariantTest.groovy
new file mode 100644
index 0000000..3696ec0
--- /dev/null
+++ b/src/test/groovy/transform/NamedVariantTest.groovy
@@ -0,0 +1,86 @@
+/*
+ *  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 java.lang.reflect.Modifier
+import groovy.test.GroovyTestCase
+
+/**
+ * Unit tests for the NamedVariant annotation
+ */
+class NamedVariantTest extends GroovyTestCase {
+    void testMethod() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''class MyClass {
+                  |    @groovy.transform.NamedVariant
+                  |    void run(int number) {
+                  |    }
+                  |}'''.stripMargin())
+        // Should have such method `void run(Map)`
+        def method = tester.getDeclaredMethod("run", Map)
+        assert method
+        assert Modifier.isPublic(method.modifiers)
+        assert method.returnType == void.class
+    }
+
+    void testMethodCall() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''class MyClass {
+                  |    @groovy.transform.NamedVariant
+                  |    int run(int number) {
+                  |        number
+                  |    }
+                  |}'''.stripMargin()).getConstructor().newInstance()
+
+        assert tester.run(number: 123) == 123
+        try {
+            tester.run(number: "123")
+        } catch (MissingMethodException ignored) {
+            return
+        }
+        fail("Should have thrown MissingMethodException")
+    }
+
+    void testCoerceMethodCall() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''class MyClass {
+                  |    @groovy.transform.NamedVariant(coerce = true)
+                  |    int run(int number) {
+                  |        number
+                  |    }
+                  |}'''.stripMargin()).getConstructor().newInstance()
+
+        assert tester.run(number: 123) == 123
+        assert tester.run(number: "123") == 123
+    }
+
+    void testStaticCoerceMethodCall() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.CompileStatic
+                  |class MyClass {
+                  |    @groovy.transform.NamedVariant(coerce = true)
+                  |    int run(int number) {
+                  |        number
+                  |    }
+                  |}'''.stripMargin()).getConstructor().newInstance()
+
+        assert tester.run(number: 123) == 123
+        assert tester.run(number: "123") == 123
+    }
+}