You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2018/02/20 02:32:26 UTC

groovy git commit: GROOVY-7956: Provide an AST transformation which improves named parameter support

Repository: groovy
Updated Branches:
  refs/heads/master fc01573ff -> 43f2c8e47


GROOVY-7956: Provide an AST transformation which improves named parameter support


Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/43f2c8e4
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/43f2c8e4
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/43f2c8e4

Branch: refs/heads/master
Commit: 43f2c8e47b2072e2c0e42097c4462180af30efdb
Parents: fc01573
Author: paulk <pa...@asert.com.au>
Authored: Tue Feb 20 10:09:59 2018 +1000
Committer: paulk <pa...@asert.com.au>
Committed: Tue Feb 20 11:17:23 2018 +1000

----------------------------------------------------------------------
 .../groovy/groovy/transform/NamedDelegate.java  |  29 +++
 .../groovy/groovy/transform/NamedParam.java     |  34 +++
 .../groovy/groovy/transform/NamedParams.java    |  30 +++
 .../groovy/groovy/transform/NamedVariant.java   |  34 +++
 .../codehaus/groovy/ast/tools/GeneralUtils.java |   4 +
 .../transform/AbstractASTTransformation.java    |   4 +-
 .../NamedVariantASTTransformation.java          | 223 +++++++++++++++++++
 .../transform/NamedVariantTransformTest.groovy  | 124 +++++++++++
 8 files changed, 480 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/groovy/blob/43f2c8e4/src/main/groovy/groovy/transform/NamedDelegate.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/transform/NamedDelegate.java b/src/main/groovy/groovy/transform/NamedDelegate.java
new file mode 100644
index 0000000..baf54ff
--- /dev/null
+++ b/src/main/groovy/groovy/transform/NamedDelegate.java
@@ -0,0 +1,29 @@
+/*
+ *  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.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.PARAMETER)
+public @interface NamedDelegate {
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/43f2c8e4/src/main/groovy/groovy/transform/NamedParam.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/transform/NamedParam.java b/src/main/groovy/groovy/transform/NamedParam.java
new file mode 100644
index 0000000..856e3e2
--- /dev/null
+++ b/src/main/groovy/groovy/transform/NamedParam.java
@@ -0,0 +1,34 @@
+/*
+ *  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.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+@Repeatable(NamedParams.class)
+public @interface NamedParam {
+    String value();
+    Class type() default Object.class;
+    boolean required() default false;
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/43f2c8e4/src/main/groovy/groovy/transform/NamedParams.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/transform/NamedParams.java b/src/main/groovy/groovy/transform/NamedParams.java
new file mode 100644
index 0000000..646b66d
--- /dev/null
+++ b/src/main/groovy/groovy/transform/NamedParams.java
@@ -0,0 +1,30 @@
+/*
+ *  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.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface NamedParams {
+    NamedParam[] value();
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/43f2c8e4/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
new file mode 100644
index 0000000..8db0529
--- /dev/null
+++ b/src/main/groovy/groovy/transform/NamedVariant.java
@@ -0,0 +1,34 @@
+/*
+ *  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.apache.groovy.lang.annotation.Incubating;
+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;
+
+@Incubating
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.NamedVariantASTTransformation")
+public @interface NamedVariant {
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/groovy/blob/43f2c8e4/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
----------------------------------------------------------------------
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 875cc48..b45952e 100644
--- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
+++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
@@ -193,6 +193,10 @@ public class GeneralUtils {
         return new CastExpression(type, expression);
     }
 
+    public static BooleanExpression boolX(Expression boolExpr) {
+        return new BooleanExpression(boolExpr);
+    }
+
     public static CastExpression castX(ClassNode type, Expression expression, boolean ignoreAutoboxing) {
         return new CastExpression(type, expression, ignoreAutoboxing);
     }

http://git-wip-us.apache.org/repos/asf/groovy/blob/43f2c8e4/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java
index 8c1de6c..947c7f1 100644
--- a/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java
@@ -259,8 +259,8 @@ public abstract class AbstractASTTransformation implements Opcodes, ASTTransform
         return true;
     }
 
-    public static boolean hasAnnotation(ClassNode cNode, ClassNode annotation) {
-        List annots = cNode.getAnnotations(annotation);
+    public static boolean hasAnnotation(AnnotatedNode node, ClassNode annotation) {
+        List annots = node.getAnnotations(annotation);
         return (annots != null && !annots.isEmpty());
     }
 

http://git-wip-us.apache.org/repos/asf/groovy/blob/43f2c8e4/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
new file mode 100644
index 0000000..3bd0e8f
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java
@@ -0,0 +1,223 @@
+/*
+ *  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.NamedDelegate;
+import groovy.transform.NamedParam;
+import groovy.transform.NamedVariant;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.ConstructorNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.PropertyNode;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MapEntryExpression;
+import org.codehaus.groovy.ast.expr.MapExpression;
+import org.codehaus.groovy.ast.stmt.AssertStatement;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.ForStatement;
+import org.codehaus.groovy.ast.tools.GenericsUtils;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.groovy.ast.tools.ClassNodeUtils.isInnerClass;
+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.boolX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
+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.getAllProperties;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.list2args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.plusX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
+
+@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
+public class NamedVariantASTTransformation extends AbstractASTTransformation {
+    private static final Class MY_CLASS = NamedVariant.class;
+    private static final ClassNode MY_TYPE = make(MY_CLASS);
+    private static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+    private static final ClassNode NAMED_PARAM_TYPE = makeWithoutCaching(NamedParam.class, false);
+    private static final ClassNode NAMED_DELEGATE_TYPE = makeWithoutCaching(NamedDelegate.class, false);
+    private static final ClassNode MAP_TYPE = makeWithoutCaching(Map.class, false);
+
+    @Override
+    public void visit(ASTNode[] nodes, SourceUnit source) {
+        init(nodes, source);
+        MethodNode mNode = (MethodNode) nodes[1];
+        AnnotationNode anno = (AnnotationNode) nodes[0];
+        if (!MY_TYPE.equals(anno.getClassNode())) return;
+
+        Parameter[] fromParams = mNode.getParameters();
+        if (fromParams.length == 0) {
+            addError("Error during " + MY_TYPE_NAME + " processing. No-args method not supported.", mNode);
+            return;
+        }
+
+        Parameter mapParam = param(GenericsUtils.nonGeneric(ClassHelper.MAP_TYPE), "__namedArgs");
+        List<Parameter> genParams = new ArrayList<Parameter>();
+        genParams.add(mapParam);
+        ClassNode cNode = mNode.getDeclaringClass();
+        final BlockStatement inner = new BlockStatement();
+        ArgumentListExpression args = new ArgumentListExpression();
+        final List<String> propNames = new ArrayList<String>();
+
+        // first pass, just check for absence of annotations of interest
+        boolean annoFound = false;
+        for (Parameter fromParam : fromParams) {
+            if (hasAnnotation(fromParam, NAMED_PARAM_TYPE) || hasAnnotation(fromParam, NAMED_DELEGATE_TYPE)) {
+                annoFound = true;
+            }
+        }
+
+        if (!annoFound) {
+            // assume the first param is the delegate by default
+            processDelegateParam(mNode, mapParam, args, propNames, fromParams[0]);
+        } else {
+            for (Parameter fromParam : fromParams) {
+                if (hasAnnotation(fromParam, NAMED_PARAM_TYPE)) {
+                    AnnotationNode namedParam = fromParam.getAnnotations(NAMED_PARAM_TYPE).get(0);
+                    boolean required = memberHasValue(namedParam, "required", true);
+                    if (getMemberValue(namedParam, "name") == null) {
+                        namedParam.addMember("value", constX(fromParam.getName()));
+                    }
+                    String name = getMemberStringValue(namedParam, "value");
+                    if (getMemberValue(namedParam, "type") == null) {
+                        namedParam.addMember("type", classX(fromParam.getType()));
+                    }
+                    if (!checkDuplicates(mNode, propNames, name)) return;
+                    // TODO check specified type is assignable from declared param type?
+                    // ClassNode type = getMemberClassValue(namedParam, "type");
+                    if (required) {
+                        if (fromParam.hasInitialExpression()) {
+                            addError("Error during " + MY_TYPE_NAME + " processing. A required parameter can't have an initial value.", mNode);
+                            return;
+                        }
+                        inner.addStatement(new AssertStatement(boolX(callX(varX(mapParam), "containsKey", args(constX(name)))),
+                                plusX(new ConstantExpression("Missing required named argument '" + name + "'. Keys found: "), callX(varX(mapParam), "keySet"))));
+                    }
+                    args.addExpression(propX(varX(mapParam), name));
+                    mapParam.addAnnotation(namedParam);
+                    fromParam.getAnnotations().remove(namedParam);
+                } else if (hasAnnotation(fromParam, NAMED_DELEGATE_TYPE)) {
+                    if (!processDelegateParam(mNode, mapParam, args, propNames, fromParam)) return;
+                } else {
+                    args.addExpression(varX(fromParam));
+                    if (!checkDuplicates(mNode, propNames, fromParam.getName())) return;
+                    genParams.add(fromParam);
+                }
+            }
+        }
+        Parameter namedArgKey = param(STRING_TYPE, "namedArgKey");
+        inner.addStatement(
+                new ForStatement(
+                        namedArgKey,
+                        callX(varX(mapParam), "keySet"),
+                        new AssertStatement(boolX(callX(list2args(propNames), "contains", varX(namedArgKey))),
+                                plusX(new ConstantExpression("Unrecognized namedArgKey: "), varX(namedArgKey)))
+                ));
+
+        Parameter[] genParamsArray = genParams.toArray(new Parameter[genParams.size()]);
+        // TODO account for default params giving multiple signatures
+        if (cNode.hasMethod(mNode.getName(), genParamsArray)) {
+            addError("Error during " + MY_TYPE_NAME + " processing. Class " + cNode.getNameWithoutPackage() +
+                    " already has a named-arg " + (mNode instanceof ConstructorNode ? "constructor" : "method") +
+                    " of type " + genParams, mNode);
+            return;
+        }
+
+        final BlockStatement body = new BlockStatement();
+        if (mNode instanceof ConstructorNode) {
+            body.addStatement(stmt(ctorX(ClassNode.THIS, args)));
+            body.addStatement(inner);
+            cNode.addConstructor(
+                    mNode.getModifiers(),
+                    genParamsArray,
+                    mNode.getExceptions(),
+                    body
+            );
+        } else {
+            body.addStatement(inner);
+            body.addStatement(stmt(callThisX(mNode.getName(), args)));
+            cNode.addMethod(
+                    mNode.getName(),
+                    mNode.getModifiers(),
+                    mNode.getReturnType(),
+                    genParamsArray,
+                    mNode.getExceptions(),
+                    body
+            );
+        }
+    }
+
+    private boolean processDelegateParam(MethodNode mNode, Parameter mapParam, ArgumentListExpression args, List<String> propNames, Parameter fromParam) {
+        if (isInnerClass(fromParam.getType())) {
+            if (mNode.isStatic()) {
+                addError("Error during " + MY_TYPE_NAME + " processing. Delegate type '" + fromParam.getType().getNameWithoutPackage() + "' is an inner class which is not supported.", mNode);
+                return false;
+            }
+        }
+
+        Set<String> names = new HashSet<String>();
+        List<PropertyNode> props = getAllProperties(names, fromParam.getType(), true, false, false, true, false, true);
+        for (String next : names) {
+            if (!checkDuplicates(mNode, propNames, next)) return false;
+        }
+        List<MapEntryExpression> entries = new ArrayList<MapEntryExpression>();
+        for (PropertyNode pNode : props) {
+            String name = pNode.getName();
+            entries.add(new MapEntryExpression(constX(name), propX(varX(mapParam), name)));
+            AnnotationNode namedParam = new AnnotationNode(NAMED_PARAM_TYPE);
+            namedParam.addMember("value", constX(name));
+            namedParam.addMember("type", classX(pNode.getType()));
+            mapParam.addAnnotation(namedParam);
+        }
+        Expression delegateMap = new MapExpression(entries);
+        args.addExpression(castX(fromParam.getType(), delegateMap));
+        return true;
+    }
+
+    private boolean checkDuplicates(MethodNode mNode, List<String> propNames, String next) {
+        if (propNames.contains(next)) {
+            addError("Error during " + MY_TYPE_NAME + " processing. Duplicate property '" + next + "' found.", mNode);
+            return false;
+        }
+        propNames.add(next);
+        return true;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/groovy/blob/43f2c8e4/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
new file mode 100644
index 0000000..6f0bea4
--- /dev/null
+++ b/src/test/org/codehaus/groovy/transform/NamedVariantTransformTest.groovy
@@ -0,0 +1,124 @@
+/*
+ *  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
+
+/**
+ * Tests for the {@code @NamedVariant} transformation.
+ */
+class NamedVariantTransformTest extends GroovyShellTestCase {
+
+    void testNamedParam() {
+        assertScript '''
+            import groovy.transform.*
+
+            class Animal {
+                String type
+                String name
+            }
+
+            @ToString(includeNames=true, includeFields=true)
+            class Color {
+                Integer r, g
+                private Integer b
+                Integer setB(Integer b) { this.b = b }
+            }
+
+            @NamedVariant
+            String foo(a, @NamedParam String b2, @NamedDelegate Color shade, int c, @NamedParam(required=true) d, @NamedDelegate Animal pet) {
+              "$a $b2 $c $d ${pet.type?.toUpperCase()}:$pet.name $shade"
+            }
+
+            def result = foo(b2: 'b param', g: 12, b: 42, r: 12, 'foo', 42, d:true, type: 'Dog', name: 'Rover')
+            assert result == 'foo b param 42 true DOG:Rover Color(r:12, g:12, b:42)'
+        '''
+    }
+
+    void testNamedDelegate() {
+        assertScript """
+            import groovy.transform.*
+
+            @ToString(includeNames=true, includeFields=true)
+            class Color {
+                Integer r, g, b
+            }
+
+            @NamedVariant
+            String foo(Color shade) {
+              shade
+            }
+
+            def result = foo(g: 12, b: 42, r: 12)
+            assert result == 'Color(r:12, g:12, b:42)'
+        """
+    }
+
+    void testNamedParamConstructor() {
+        assertScript """
+            import groovy.transform.*
+
+            @ToString(includeNames=true, includeFields=true)
+            class Color {
+                @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
+            }
+
+            assert new Color(r: 10, g: 20, b: 30).toString() == 'Color(r:10, g:20, b:30)'
+        """
+    }
+
+    void testNamedParamInnerClass() {
+        assertScript '''
+            import groovy.transform.*
+
+            class Foo {
+                int adjust
+                @ToString(includeNames = true)
+                class Bar {
+                    @NamedVariant
+                    Bar(@NamedParam int x, @NamedParam int y) {
+                        this.x = x + adjust
+                        this.y = y + adjust
+                    }
+                    int x, y
+                    @NamedVariant
+                    def update(@NamedParam int x, @NamedParam int y) {
+                        this.x = x + adjust
+                        this.y = y + adjust
+                    }
+                }
+                def makeBar() {
+                    new Bar(x: 0, y: 0)
+                }
+            }
+
+            def b = new Foo(adjust: 1).makeBar()
+            assert b.toString() == 'Foo$Bar(x:1, y:1)'
+            b.update(10, 10)
+            assert b.toString() == 'Foo$Bar(x:11, y:11)'
+            b.update(x:15, y:25)
+            assert b.toString() == 'Foo$Bar(x:16, y:26)'
+        '''
+    }
+
+}