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