You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@groovy.apache.org by pa...@apache.org on 2015/05/23 03:15:35 UTC

[1/2] incubator-groovy git commit: GROOVY-7353: Groovy should provide a MapConstructor AST transform

Repository: incubator-groovy
Updated Branches:
  refs/heads/master 14a3a6700 -> 569d68a9b


GROOVY-7353: Groovy should provide a MapConstructor AST transform


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

Branch: refs/heads/master
Commit: 87b6633f53a8a7575dba9b0cd27a691cb38e320a
Parents: 14a3a67
Author: Paul King <pa...@asert.com.au>
Authored: Wed May 20 12:10:46 2015 +1000
Committer: Paul King <pa...@asert.com.au>
Committed: Sat May 23 10:07:03 2015 +1000

----------------------------------------------------------------------
 src/main/groovy/transform/MapConstructor.java   | 121 ++++++++++
 .../groovy/antlr/AntlrParserPlugin.java         |   7 +-
 .../MapConstructorASTTransformation.java        | 225 +++++++++++++++++++
 .../MapConstructorTransformTest.groovy          | 139 ++++++++++++
 4 files changed, 491 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/87b6633f/src/main/groovy/transform/MapConstructor.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/transform/MapConstructor.java b/src/main/groovy/transform/MapConstructor.java
new file mode 100644
index 0000000..adcab87
--- /dev/null
+++ b/src/main/groovy/transform/MapConstructor.java
@@ -0,0 +1,121 @@
+/*
+ *  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.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Class annotation used to assist in the creation of map constructors in classes.
+ * <p>
+ * It allows you to write classes in this shortened form:
+ * <pre>
+ * import groovy.transform.*
+ *
+ * {@code @TupleConstructor}
+ * class Person {
+ *     String first, last
+ * }
+ *
+ * {@code @CompileStatic // optional
+ * {@code @ToString(includeSuperProperties=true)}
+ * {@code @MapConstructor}(pre={ super(args?.first, args?.last); args = args ?: [:] }, post = { first = first?.toUpperCase() })
+ * class Author extends Person {
+ *     String bookName
+ * }
+ *
+ * assert new Author(first: 'Dierk', last: 'Koenig', bookName: 'ReGinA').toString() == 'Author(ReGinA, DIERK, Koenig)'
+ * assert new Author().toString() == 'Author(null, null, null)'
+ * </pre>
+ * The {@code @MapConstructor} annotation instructs the compiler to execute an
+ * AST transformation which adds the necessary constructor method to your class.
+ * <p>
+ * A map constructor is created which sets properties, and optionally fields and
+ * super properties if the property/field name is a key within the map.
+ * <p>
+ * For the above example, the generated constructor will be something like:
+ * <pre>
+ * public Author(java.util.Map args) {
+ *     super(args?.first, args?.last)
+ *     args = args ? args : [:]
+ *     if (args.containsKey('bookName')) {
+ *         this.bookName = args['bookName']
+ *     }
+ *     first = first?.toUpperCase()
+ * }
+ * </pre>
+ *
+ * @since 2.5.0
+ */
+@java.lang.annotation.Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.MapConstructorASTTransformation")
+public @interface MapConstructor {
+    /**
+     * List of field and/or property names to exclude from the constructor.
+     * Must not be used if 'includes' is used. For convenience, a String with comma separated names
+     * can be used in addition to an array (using Groovy's literal list notation) of String values.
+     */
+    String[] excludes() default {};
+
+    /**
+     * List of field and/or property names to include within the constructor.
+     * Must not be used if 'excludes' is used. For convenience, a String with comma separated names
+     * can be used in addition to an array (using Groovy's literal list notation) of String values.
+     */
+    String[] includes() default {};
+
+    /**
+     * Include fields in the constructor.
+     */
+    boolean includeFields() default false;
+
+    /**
+     * Include properties in the constructor.
+     */
+    boolean includeProperties() default true;
+
+    /**
+     * Include properties from super classes in the constructor.
+     */
+    boolean includeSuperProperties() default false;
+
+    /**
+     * By default, properties are set directly using their respective field.
+     * By setting {@code useSetters=true} then a writable property will be set using its setter.
+     * If turning on this flag we recommend that setters that might be called are
+     * made null-safe wrt the parameter.
+     */
+    boolean useSetters() default false;
+
+    /**
+     * A Closure containing statements which will be prepended to the generated constructor. The first statement within the Closure may be "super(someArgs)" in which case the no-arg super constructor won't be called.
+     */
+    Class pre();
+
+    /**
+     * A Closure containing statements which will be appended to the end of the generated constructor. Useful for validation steps or tweaking the populated fields/properties.
+     */
+    Class post();
+}

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/87b6633f/src/main/org/codehaus/groovy/antlr/AntlrParserPlugin.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/antlr/AntlrParserPlugin.java b/src/main/org/codehaus/groovy/antlr/AntlrParserPlugin.java
index 0527e16..d225ee6 100644
--- a/src/main/org/codehaus/groovy/antlr/AntlrParserPlugin.java
+++ b/src/main/org/codehaus/groovy/antlr/AntlrParserPlugin.java
@@ -104,6 +104,7 @@ public class AntlrParserPlugin extends ASTHelper implements ParserPlugin, Groovy
     private int innerClassCounter = 1;
     private boolean enumConstantBeingDef = false;
     private boolean forStatementBeingDef = false;
+    private boolean annotationBeingDef = false;
     private boolean firstParamIsVarArg = false;
     private boolean firstParam = false;
 
@@ -1226,6 +1227,7 @@ public class AntlrParserPlugin extends ASTHelper implements ParserPlugin, Groovy
     }
 
     protected AnnotationNode annotation(AST annotationNode) {
+        annotationBeingDef = true;
         AST node = annotationNode.getFirstChild();
         String name = qualifiedName(node);
         AnnotationNode annotatedNode = new AnnotationNode(ClassHelper.make(name));
@@ -1244,6 +1246,7 @@ public class AntlrParserPlugin extends ASTHelper implements ParserPlugin, Groovy
                 break;
             }
         }
+        annotationBeingDef = false;
         return annotatedNode;
     }
 
@@ -2490,7 +2493,9 @@ public class AntlrParserPlugin extends ASTHelper implements ParserPlugin, Groovy
         // if node text is found to be "super"/"this" when a method call is being processed, it is a 
         // call like this(..)/super(..) after the first statement, which shouldn't be allowed. GROOVY-2836
         if (selector.getText().equals("this") || selector.getText().equals("super")) {
-            throw new ASTRuntimeException(elist, "Constructor call must be the first statement in a constructor.");
+            if (!(annotationBeingDef && selector.getText().equals("super"))) {
+                throw new ASTRuntimeException(elist, "Constructor call must be the first statement in a constructor.");
+            }
         }
 
         Expression arguments = arguments(elist);

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/87b6633f/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java b/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
new file mode 100644
index 0000000..ecbbd9c
--- /dev/null
+++ b/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
@@ -0,0 +1,225 @@
+/*
+ *  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.MapConstructor;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotatedNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.ConstructorNode;
+import org.codehaus.groovy.ast.DynamicVariable;
+import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.EmptyStatement;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.classgen.Verifier;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+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.assignS;
+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.constX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceNonPropertyFields;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstancePropertyFields;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.getSuperPropertyFields;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
+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;
+
+/**
+ * Handles generation of code for the @MapConstructor annotation.
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class MapConstructorASTTransformation extends AbstractASTTransformation {
+
+    static final Class MY_CLASS = MapConstructor.class;
+    static final ClassNode MY_TYPE = make(MY_CLASS);
+    static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+    private static final ClassNode MAP_TYPE = makeWithoutCaching(Map.class, false);
+//    private static final ClassNode CHECK_METHOD_TYPE = make(ImmutableASTTransformation.class);
+
+    public void visit(ASTNode[] nodes, SourceUnit source) {
+        init(nodes, source);
+        AnnotatedNode parent = (AnnotatedNode) nodes[1];
+        AnnotationNode anno = (AnnotationNode) nodes[0];
+        if (!MY_TYPE.equals(anno.getClassNode())) return;
+
+        if (parent instanceof ClassNode) {
+            ClassNode cNode = (ClassNode) parent;
+            if (!checkNotInterface(cNode, MY_TYPE_NAME)) return;
+            boolean includeFields = memberHasValue(anno, "includeFields", true);
+            boolean includeProperties = !memberHasValue(anno, "includeProperties", false);
+            boolean includeSuperProperties = memberHasValue(anno, "includeSuperProperties", true);
+            boolean useSetters = memberHasValue(anno, "useSetters", true);
+            List<String> excludes = getMemberList(anno, "excludes");
+            List<String> includes = getMemberList(anno, "includes");
+            if (hasAnnotation(cNode, CanonicalASTTransformation.MY_TYPE)) {
+                AnnotationNode canonical = cNode.getAnnotations(CanonicalASTTransformation.MY_TYPE).get(0);
+                if (excludes == null || excludes.isEmpty()) excludes = getMemberList(canonical, "excludes");
+                if (includes == null || includes.isEmpty()) includes = getMemberList(canonical, "includes");
+            }
+            if (!checkIncludeExclude(anno, excludes, includes, MY_TYPE_NAME)) return;
+            // if @Immutable is found, let it pick up options and do work so we'll skip
+            if (hasAnnotation(cNode, ImmutableASTTransformation.MY_TYPE)) return;
+
+            Expression pre = anno.getMember("pre");
+            if (pre != null && !(pre instanceof ClosureExpression)) {
+                addError("Expected closure value for annotation parameter 'pre'. Found " + pre, cNode);
+                return;
+            }
+            Expression post = anno.getMember("post");
+            if (post != null && !(post instanceof ClosureExpression)) {
+                addError("Expected closure value for annotation parameter 'post'. Found " + post, cNode);
+                return;
+            }
+
+            createConstructor(cNode, includeFields, includeProperties, includeSuperProperties, useSetters, excludes, includes, (ClosureExpression) pre, (ClosureExpression) post, source);
+            if (pre != null) {
+                anno.setMember("pre", new ClosureExpression(new Parameter[0], new EmptyStatement()));
+            }
+            if (post != null) {
+                anno.setMember("post", new ClosureExpression(new Parameter[0], new EmptyStatement()));
+            }
+        }
+    }
+
+    public static void createConstructor(ClassNode cNode, boolean includeFields, boolean includeProperties, boolean includeSuperProperties, boolean useSetters, List<String> excludes, List<String> includes, ClosureExpression pre, ClosureExpression post, SourceUnit source) {
+        List<ConstructorNode> constructors = cNode.getDeclaredConstructors();
+        boolean foundEmpty = constructors.size() == 1 && constructors.get(0).getFirstStatement() == null;
+        // HACK: JavaStubGenerator could have snuck in a constructor we don't want
+        if (foundEmpty) constructors.remove(0);
+
+        List<FieldNode> superList = new ArrayList<FieldNode>();
+        if (includeSuperProperties) {
+            superList.addAll(getSuperPropertyFields(cNode.getSuperClass()));
+        }
+
+        List<FieldNode> list = new ArrayList<FieldNode>();
+        if (includeProperties) {
+            list.addAll(getInstancePropertyFields(cNode));
+        }
+        if (includeFields) {
+            list.addAll(getInstanceNonPropertyFields(cNode));
+        }
+
+        Parameter map = param(MAP_TYPE, "args");
+        final BlockStatement body = new BlockStatement();
+        ClassCodeExpressionTransformer transformer = makeTransformer();
+        if (pre != null) {
+            ClosureExpression transformed = (ClosureExpression) transformer.transform(pre);
+            copyPreStatements(transformed, body);
+        }
+        for (FieldNode fNode : superList) {
+            String name = fNode.getName();
+            if (shouldSkip(name, excludes, includes)) continue;
+            assignField(useSetters, map, body, name);
+        }
+        for (FieldNode fNode : list) {
+            String name = fNode.getName();
+            if (shouldSkip(name, excludes, includes)) continue;
+            assignField(useSetters, map, body, name);
+        }
+        if (post != null) {
+            ClosureExpression transformed = (ClosureExpression) transformer.transform(post);
+            body.addStatement(transformed.getCode());
+        }
+        cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, params(map), ClassNode.EMPTY_ARRAY, body));
+    }
+
+    private static void assignField(boolean useSetters, Parameter map, BlockStatement body, String name) {
+        ArgumentListExpression nameArg = args(constX(name));
+        body.addStatement(ifS(callX(varX(map), "containsKey", nameArg), useSetters ?
+                stmt(callThisX(getSetterName(name), callX(varX(map), "get", nameArg))) :
+                assignS(propX(varX("this"), name), callX(varX(map), "get", nameArg))));
+    }
+
+    private static String getSetterName(String name) {
+        return "set" + Verifier.capitalize(name);
+    }
+
+    private static ClassCodeExpressionTransformer makeTransformer() {
+        return new ClassCodeExpressionTransformer() {
+                @Override
+                public Expression transform(Expression exp) {
+                    if (exp instanceof ClosureExpression) {
+                        ClosureExpression ce = (ClosureExpression) exp;
+                        ce.getCode().visit(this);
+                    } else if (exp instanceof VariableExpression) {
+                        VariableExpression ve = (VariableExpression) exp;
+                        if (ve.getName().equals("args") && ve.getAccessedVariable() instanceof DynamicVariable) {
+                            VariableExpression newVe = new VariableExpression(new Parameter(MAP_TYPE, "args"));
+                            newVe.setSourcePosition(ve);
+                            return newVe;
+                        }
+                    }
+                    return exp.transformExpression(this);
+                }
+
+                @Override
+                protected SourceUnit getSourceUnit() {
+                    return null;
+                }
+            };
+    }
+
+    private static void copyPreStatements(ClosureExpression pre, BlockStatement body) {
+        Statement preCode = pre.getCode();
+        if (preCode instanceof BlockStatement) {
+            BlockStatement block = (BlockStatement) preCode;
+            List<Statement> statements = block.getStatements();
+            for (int i = 0; i < statements.size(); i++) {
+                Statement statement = statements.get(i);
+                if (i == 0 && statement instanceof ExpressionStatement) {
+                    ExpressionStatement es = (ExpressionStatement) statement;
+                    Expression preExp = es.getExpression();
+                    if (preExp instanceof MethodCallExpression) {
+                        MethodCallExpression mce = (MethodCallExpression) preExp;
+                        String name = mce.getMethodAsString();
+                        if ("super".equals(name)) {
+                            es.setExpression(new ConstructorCallExpression(ClassNode.SUPER, mce.getArguments()));
+                        }
+                    }
+                }
+                body.addStatement(statement);
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/87b6633f/src/test/org/codehaus/groovy/transform/MapConstructorTransformTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/org/codehaus/groovy/transform/MapConstructorTransformTest.groovy b/src/test/org/codehaus/groovy/transform/MapConstructorTransformTest.groovy
new file mode 100644
index 0000000..22cd930
--- /dev/null
+++ b/src/test/org/codehaus/groovy/transform/MapConstructorTransformTest.groovy
@@ -0,0 +1,139 @@
+/*
+ *  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
+
+class MapConstructorTransformTest extends GroovyTestCase {
+    void testMapConstructorWithFinalFields() {
+        assertScript '''
+            import groovy.transform.*
+
+            @ToString
+            @MapConstructor
+            class Person {
+                final String first, last
+            }
+
+            assert new Person(first: 'Dierk', last: 'Koenig').toString() == 'Person(Dierk, Koenig)'
+        '''
+    }
+
+    void testMapConstructorWithSetters() {
+        assertScript '''
+            import groovy.transform.*
+
+            @ToString
+            @MapConstructor(useSetters=true)
+            class Person {
+                String first, last
+                void setFirst(String first) {
+                    this.first = first?.toUpperCase()
+                }
+            }
+
+            assert new Person(first: 'Dierk', last: 'Koenig').toString() == 'Person(DIERK, Koenig)'
+        '''
+    }
+
+    void testMapConstructorWithIncludesAndExcludes() {
+        assertScript '''
+            import groovy.transform.*
+
+            @ToString(includes='first')
+            @MapConstructor(includes='first')
+            class Person {
+                String first, last
+            }
+
+            assert new Person(first: 'Dierk').toString() == 'Person(Dierk)'
+        '''
+        assertScript '''
+            import groovy.transform.*
+
+            @ToString @MapConstructor(includes='first')
+            class Person {
+                String first, last
+            }
+
+            assert new Person(first: 'Dierk', last: 'Koenig').toString() == 'Person(Dierk, null)'
+        '''
+        assertScript '''
+            import groovy.transform.*
+
+            @ToString @MapConstructor(excludes='last')
+            class Person {
+                String first, last
+            }
+
+            assert new Person(first: 'Dierk', last: 'Koenig').toString() == 'Person(Dierk, null)'
+        '''
+    }
+
+    void testMapConstructorWithPost() {
+        def msg = shouldFail(MissingPropertyException, '''
+            import groovy.transform.*
+            import org.codehaus.groovy.transform.ImmutableASTTransformation
+
+            @ToString
+            @MapConstructor(post={ ImmutableASTTransformation.checkPropNames(this, args) })
+            class Person {
+                String first, last
+            }
+
+            new Person(last: 'Koenig', nickname: 'mittie')
+        ''')
+        assert msg.contains('No such property: nickname for class: Person')
+    }
+
+    void testMapConstructorWithPostAndFields() {
+        assertScript '''
+            import groovy.transform.*
+
+            @ToString(includeFields=true, includeNames=true)
+            @MapConstructor(includeFields=true, post={ full = "$first $last" })
+            class Person {
+                final String first, last
+                private final String full
+            }
+
+            assert new Person(first: 'Dierk', last: 'Koenig').toString() ==
+                'Person(first:Dierk, last:Koenig, full:Dierk Koenig)'
+        '''
+    }
+
+    void testMapConstructorWithPreAndPost() {
+        assertScript '''
+            import groovy.transform.*
+
+            @TupleConstructor
+            class Person {
+                String first, last
+            }
+
+            @CompileStatic // optional
+            @ToString(includeSuperProperties=true)
+            @MapConstructor(pre={ super(args?.first, args?.last); args = args ?: [:] }, post = { first = first?.toUpperCase() })
+            class Author extends Person {
+                String bookName
+            }
+
+            assert new Author(first: 'Dierk', last: 'Koenig', bookName: 'ReGinA').toString() == 'Author(ReGinA, DIERK, Koenig)'
+            assert new Author().toString() == 'Author(null, null, null)'
+        '''
+    }
+}


[2/2] incubator-groovy git commit: GROOVY-7353: Groovy should provide a MapConstructor AST transform - additional tests and mods for recent master changes (closes #20)

Posted by pa...@apache.org.
GROOVY-7353: Groovy should provide a MapConstructor AST transform - additional tests and mods for recent master changes (closes #20)


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

Branch: refs/heads/master
Commit: 569d68a9ba5ef7ff6cea683203a70646211fdee9
Parents: 87b6633
Author: Paul King <pa...@asert.com.au>
Authored: Sat May 23 11:15:01 2015 +1000
Committer: Paul King <pa...@asert.com.au>
Committed: Sat May 23 11:15:01 2015 +1000

----------------------------------------------------------------------
 .../MapConstructorASTTransformation.java        |  7 +--
 .../MapConstructorTransformTest.groovy          | 52 +++++++++++++++++++-
 2 files changed, 53 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/569d68a9/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java b/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
index ecbbd9c..c968a0c 100644
--- a/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
+++ b/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
@@ -90,12 +90,9 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation {
             boolean useSetters = memberHasValue(anno, "useSetters", true);
             List<String> excludes = getMemberList(anno, "excludes");
             List<String> includes = getMemberList(anno, "includes");
-            if (hasAnnotation(cNode, CanonicalASTTransformation.MY_TYPE)) {
-                AnnotationNode canonical = cNode.getAnnotations(CanonicalASTTransformation.MY_TYPE).get(0);
-                if (excludes == null || excludes.isEmpty()) excludes = getMemberList(canonical, "excludes");
-                if (includes == null || includes.isEmpty()) includes = getMemberList(canonical, "includes");
-            }
             if (!checkIncludeExclude(anno, excludes, includes, MY_TYPE_NAME)) return;
+            if (!checkPropertyList(cNode, includes, "includes", anno, MY_TYPE_NAME, includeFields)) return;
+            if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields)) return;
             // if @Immutable is found, let it pick up options and do work so we'll skip
             if (hasAnnotation(cNode, ImmutableASTTransformation.MY_TYPE)) return;
 

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/569d68a9/src/test/org/codehaus/groovy/transform/MapConstructorTransformTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/org/codehaus/groovy/transform/MapConstructorTransformTest.groovy b/src/test/org/codehaus/groovy/transform/MapConstructorTransformTest.groovy
index 22cd930..f59c26b 100644
--- a/src/test/org/codehaus/groovy/transform/MapConstructorTransformTest.groovy
+++ b/src/test/org/codehaus/groovy/transform/MapConstructorTransformTest.groovy
@@ -18,7 +18,7 @@
  */
 package org.codehaus.groovy.transform
 
-class MapConstructorTransformTest extends GroovyTestCase {
+class MapConstructorTransformTest extends GroovyShellTestCase {
     void testMapConstructorWithFinalFields() {
         assertScript '''
             import groovy.transform.*
@@ -136,4 +136,54 @@ class MapConstructorTransformTest extends GroovyTestCase {
             assert new Author().toString() == 'Author(null, null, null)'
         '''
     }
+
+    void testIncludesAndExcludesTogetherResultsInError() {
+        def message = shouldFail {
+            evaluate """
+                import groovy.transform.MapConstructor
+
+                @MapConstructor(includes='surName', excludes='surName')
+                class Person {
+                    String surName
+                }
+
+                new Person()
+            """
+        }
+        assert message.contains("Error during @MapConstructor processing: Only one of 'includes' and 'excludes' should be supplied not both.")
+    }
+
+    void testIncludesWithInvalidPropertyNameResultsInError() {
+        def message = shouldFail {
+            evaluate """
+                import groovy.transform.MapConstructor
+
+                @MapConstructor(includes='sirName')
+                class Person {
+                    String firstName
+                    String surName
+                }
+
+                new Person(surname: "Doe")
+            """
+        }
+        assert message.contains("Error during @MapConstructor processing: 'includes' property 'sirName' does not exist.")
+    }
+
+    void testExcludesWithInvalidPropertyNameResultsInError() {
+        def message = shouldFail {
+            evaluate """
+                import groovy.transform.MapConstructor
+
+                @MapConstructor(excludes='sirName')
+                class Person {
+                    String firstName
+                    String surName
+                }
+
+                new Person(surname: "Doe")
+            """
+        }
+        assert message.contains("Error during @MapConstructor processing: 'excludes' property 'sirName' does not exist.")
+    }
 }