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/22 07:20:05 UTC

groovy git commit: GROOVY-7981: Not public constructors for groovy.transform.Immutable annotated class (closes #453, closes #665)

Repository: groovy
Updated Branches:
  refs/heads/master 40d337def -> a98efdb3f


GROOVY-7981: Not public constructors for groovy.transform.Immutable annotated class (closes #453, closes #665)


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

Branch: refs/heads/master
Commit: a98efdb3f2eb5453e1d541cef9ce7dff9e811331
Parents: 40d337d
Author: paulk <pa...@asert.com.au>
Authored: Thu Feb 22 15:25:07 2018 +1000
Committer: paulk <pa...@asert.com.au>
Committed: Thu Feb 22 17:17:58 2018 +1000

----------------------------------------------------------------------
 .../groovy/groovy/transform/MapConstructor.java | 21 +++++++
 .../groovy/groovy/transform/NamedVariant.java   | 15 ++++-
 .../groovy/transform/TupleConstructor.java      | 21 +++++++
 .../transform/options/PropertyHandler.java      |  5 ++
 .../groovy/transform/options/Visibility.java    | 25 ++++++--
 .../groovy/ast/tools/VisibilityUtils.java       | 40 ++++++-------
 .../groovy/classgen/EnumCompletionVisitor.java  |  7 ++-
 .../MapConstructorASTTransformation.java        | 10 ++--
 .../NamedVariantASTTransformation.java          | 15 +++--
 .../TupleConstructorASTTransformation.java      | 19 +++---
 src/spec/doc/core-metaprogramming.adoc          |  7 +++
 .../test/CodeGenerationASTTransformsTest.groovy |  5 ++
 .../TupleConstructorTransformTest.groovy        | 63 ++++++++++++++++++++
 13 files changed, 200 insertions(+), 53 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/main/groovy/groovy/transform/MapConstructor.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/transform/MapConstructor.java b/src/main/groovy/groovy/transform/MapConstructor.java
index d6aec1c..8c13284 100644
--- a/src/main/groovy/groovy/transform/MapConstructor.java
+++ b/src/main/groovy/groovy/transform/MapConstructor.java
@@ -67,6 +67,19 @@ import java.lang.annotation.Target;
  * }
  * </pre>
  * <p>
+ * Custom visibility:
+ * <ul>
+ * <li>The {@code @MapConstructor} annotation generates a public constructor unless an applicable
+ * {@link VisibilityOptions} annotation is also present. It can be useful to change the visibility
+ * if you want to also create a builder or provide your own static factory method for object creation.
+ * You can make the constructor private and access it from the builder or your factory method. (Note:
+ * you'll probably want to use {@code @CompileStatic} in conjunction with such an approach since
+ * dynamic Groovy currently gives the ability to access even private constructors.)</li>
+ * <li>An optional {@code visibilityId} attribute can be specified. If present, it must match the optional
+ * {@code id} attribute of an applicable {@code VisibilityOptions} annotation. This can be useful
+ * if multiple {@code VisibilityOptions} annotations are needed.</li>
+ * </ul>
+ * <p>
  * Custom property handling:
  * <ul>
  * <li>The {@code @MapConstructor} annotation supports customization using {@code @PropertyOptions}
@@ -92,6 +105,7 @@ import java.lang.annotation.Target;
  * </ul>
  *
  * @see PropertyOptions
+ * @see VisibilityOptions
  * @since 2.5.0
  */
 @java.lang.annotation.Documented
@@ -179,6 +193,13 @@ public @interface MapConstructor {
     boolean specialNamedArgHandling() default true;
 
     /**
+     * If specified, must match the "id" attribute in a VisibilityOptions annotation to enable a custom visibility.
+     *
+     * @since 2.5.0
+     */
+    String visibilityId() default Undefined.STRING;
+
+    /**
      * 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() default Undefined.CLASS.class;

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/main/groovy/groovy/transform/NamedVariant.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/transform/NamedVariant.java b/src/main/groovy/groovy/transform/NamedVariant.java
index 2a10470..e3823cf 100644
--- a/src/main/groovy/groovy/transform/NamedVariant.java
+++ b/src/main/groovy/groovy/transform/NamedVariant.java
@@ -52,6 +52,8 @@ import java.lang.annotation.Target;
  *     <li>If no arguments with {@code @NamedParam} or {@code @NamedDelegate} annotations are found the
  *     first argument is assumed to be an implicit named delegate</li>
  * </ol>
+ * You can also mix and match the {@code @NamedParam} and {@code @NamedDelegate} annotations.
+ *
  * Named arguments will be supplied via the map with their property name (configurable via
  * annotation attributes within {@code @NamedParam}) being the key and value being the argument value.
  * For named delegates, any properties of the delegate can become map keys. Duplicate keys across
@@ -75,15 +77,22 @@ import java.lang.annotation.Target;
  * def result = foo(g: 12, b: 42, r: 12)
  * assert result.toString() == 'Color(r:12, g:12, b:42)'
  * </pre>
+ * You could also explicitly annotate the {@code shade} argument with the {@code @NamedDelegate} annotation if you wanted.
+ *
  * The generated method will be something like this:
  * <pre>
  * String foo(Map args) {
  *     return foo(args as Color)
  * }
  * </pre>
- * The generated method/constructor retains the visibility and return type of the original
- * but the {@code @VisibilityOptions} annotation can be added to the visibility. You could have the
+ * The generated method/constructor retains the visibility and return type of the original method/constructor
+ * but the {@link @VisibilityOptions} annotation can be added to customize the visibility. You could have the
  * annotated method/constructor private for instance but have the generated one be public.
+ *
+ * @see VisibilityOptions
+ * @see NamedParam
+ * @see NamedDelegate
+ * @since 2.5.0
  */
 @Incubating
 @Retention(RetentionPolicy.SOURCE)
@@ -91,7 +100,7 @@ import java.lang.annotation.Target;
 @GroovyASTTransformationClass("org.codehaus.groovy.transform.NamedVariantASTTransformation")
 public @interface NamedVariant {
     /**
-     * If specified, must match the "id" attribute in the VisibilityOptions annotation.
+     * If specified, must match the optional "id" attribute in an applicable {@code VisibilityOptions} annotation.
      */
     String visibilityId() default Undefined.STRING;
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/main/groovy/groovy/transform/TupleConstructor.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/transform/TupleConstructor.java b/src/main/groovy/groovy/transform/TupleConstructor.java
index 32af45f..651f2ca 100644
--- a/src/main/groovy/groovy/transform/TupleConstructor.java
+++ b/src/main/groovy/groovy/transform/TupleConstructor.java
@@ -158,6 +158,19 @@ import java.lang.annotation.Target;
  * assert student.courses == ['IT']
  * </pre>
  * <p>
+ * Custom visibility:
+ * <ul>
+ * <li>The {@code @TupleConstructor} annotation generates a public constructor unless an applicable
+ * {@link VisibilityOptions} annotation is also present. It can be useful to change the visibility
+ * if you want to also create a builder or provide your own static factory method for object creation.
+ * You can make the constructor private and access it from the builder or your factory method. (Note:
+ * you'll probably want to use {@code @CompileStatic} in conjunction with such an approach since
+ * dynamic Groovy currently gives the ability to access even private constructors.)</li>
+ * <li>An optional {@code visibilityId} attribute can be specified. If present, it must match the optional
+ * {@code id} attribute of an applicable {@code VisibilityOptions} annotation. This can be useful
+ * if multiple {@code VisibilityOptions} annotations are needed.</li>
+ * </ul>
+ * <p>
  * Custom property handling:
  * <ul>
  * <li>The {@code @TupleConstructor} annotation supports customization using {@code @PropertyOptions}
@@ -197,6 +210,7 @@ import java.lang.annotation.Target;
  * </ul>
  *
  * @see PropertyOptions
+ * @see VisibilityOptions
  * @since 1.8.0
  */
 @java.lang.annotation.Documented
@@ -307,6 +321,13 @@ public @interface TupleConstructor {
     boolean allProperties() default false;
 
     /**
+     * If specified, must match the "id" attribute in a VisibilityOptions annotation to enable a custom visibility.
+     *
+     * @since 2.5.0
+     */
+    String visibilityId() default Undefined.STRING;
+
+    /**
      * A Closure containing statements which will be prepended to the generated constructor. The first statement
      * within the Closure may be {@code super(someArgs)} in which case the no-arg super constructor won't be called.
      *

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/main/groovy/groovy/transform/options/PropertyHandler.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/transform/options/PropertyHandler.java b/src/main/groovy/groovy/transform/options/PropertyHandler.java
index 1a4acc0..fc9029a 100644
--- a/src/main/groovy/groovy/transform/options/PropertyHandler.java
+++ b/src/main/groovy/groovy/transform/options/PropertyHandler.java
@@ -35,6 +35,11 @@ import java.util.List;
 
 import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;
 
+/**
+ * Used to provide custom property handling when getting, setting or initializing properties.
+ *
+ * @since 2.5.0
+ */
 @Incubating
 public abstract class PropertyHandler {
     private static final Class<? extends Annotation> PROPERTY_OPTIONS_CLASS = PropertyOptions.class;

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/main/groovy/groovy/transform/options/Visibility.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/transform/options/Visibility.java b/src/main/groovy/groovy/transform/options/Visibility.java
index e0ea899..d95c338 100644
--- a/src/main/groovy/groovy/transform/options/Visibility.java
+++ b/src/main/groovy/groovy/transform/options/Visibility.java
@@ -18,10 +18,25 @@
  */
 package groovy.transform.options;
 
+import java.lang.reflect.Modifier;
+
 public enum Visibility {
-    PUBLIC,
-    PROTECTED,
-    PACKAGE_PRIVATE,
-    PRIVATE,
-    UNDEFINED
+    PUBLIC(Modifier.PUBLIC),
+    PROTECTED(Modifier.PROTECTED),
+    PACKAGE_PRIVATE(0),
+    PRIVATE(Modifier.PRIVATE),
+    UNDEFINED(-1);
+
+    private final int modifier;
+
+    Visibility(int modifier) {
+        this.modifier = modifier;
+    }
+
+    public int getModifier() {
+        if (modifier == -1) {
+            throw new UnsupportedOperationException("getModifier() not supported for UNDEFINED");
+        }
+        return modifier;
+    }
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/main/java/org/apache/groovy/ast/tools/VisibilityUtils.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/groovy/ast/tools/VisibilityUtils.java b/src/main/java/org/apache/groovy/ast/tools/VisibilityUtils.java
index 46fefad..124ad35 100644
--- a/src/main/java/org/apache/groovy/ast/tools/VisibilityUtils.java
+++ b/src/main/java/org/apache/groovy/ast/tools/VisibilityUtils.java
@@ -44,52 +44,48 @@ public class VisibilityUtils {
     private VisibilityUtils() {
     }
 
-    public static int getVisibility(AnnotationNode anno, AnnotatedNode node, int originalModifiers) {
-
+    /**
+     * Determine the correct modifiers by looking for a potential @VisibilityOptions annotation.
+     *
+     * @param anno The annotation being processed (if any) which may support a 'visibilityId' attribute
+     * @param node The node being processed which may also be annotated with @VisibilityOptions
+     * @param clazz The type of node being constructed
+     * @param originalModifiers The modifier value to adjust or return if no applicable @VisibilityOptions is found
+     * @return the updated modifiers
+     */
+    public static int getVisibility(AnnotationNode anno, AnnotatedNode node, Class<? extends AnnotatedNode> clazz, int originalModifiers) {
         List<AnnotationNode> annotations = node.getAnnotations(VISIBILITY_OPTIONS_TYPE);
-        if (annotations.isEmpty()) return originalModifiers;
+        if (annotations.isEmpty() || anno == null) return originalModifiers;
 
         String visId = getMemberStringValue(anno, "visibilityId", null);
 
         Visibility vis = null;
         if (visId == null) {
-            vis = getVisForAnnotation(node, annotations.get(0), null);
+            vis = getVisForAnnotation(clazz, annotations.get(0), null);
         } else {
             for (AnnotationNode visAnno : annotations) {
-                vis = getVisForAnnotation(node, visAnno, visId);
+                vis = getVisForAnnotation(clazz, visAnno, visId);
                 if (vis != Visibility.UNDEFINED) break;
             }
         }
         if (vis == null || vis == Visibility.UNDEFINED) return originalModifiers;
 
         int result = originalModifiers & ~(ACC_PUBLIC | ACC_PROTECTED | ACC_PRIVATE);
-        switch (vis) {
-            case PUBLIC:
-                result |= ACC_PUBLIC;
-                break;
-            case PROTECTED:
-                result |= ACC_PROTECTED;
-                break;
-            case PRIVATE:
-                result |= ACC_PRIVATE;
-                break;
-
-        }
-        return result;
+        return result | vis.getModifier();
     }
 
-    private static Visibility getVisForAnnotation(AnnotatedNode node, AnnotationNode visAnno, String visId) {
+    private static Visibility getVisForAnnotation(Class<? extends AnnotatedNode> clazz, AnnotationNode visAnno, String visId) {
         Map<String, Expression> visMembers = visAnno.getMembers();
         if (visMembers == null) return Visibility.UNDEFINED;
         String id = getMemberStringValue(visAnno, "id", null);
         if ((id == null && visId != null) || (id != null && !id.equals(visId))) return Visibility.UNDEFINED;
 
         Visibility vis = null;
-        if (node instanceof ConstructorNode) {
+        if (clazz.equals(ConstructorNode.class)) {
             vis = getVisibility(visMembers.get("constructor"));
-        } else if (node instanceof MethodNode) {
+        } else if (clazz.equals(MethodNode.class)) {
             vis = getVisibility(visMembers.get("method"));
-        } else if (node instanceof ClassNode) {
+        } else if (clazz.equals(ClassNode.class)) {
             vis = getVisibility(visMembers.get("type"));
         }
         if (vis == null || vis == Visibility.UNDEFINED) {

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/main/java/org/codehaus/groovy/classgen/EnumCompletionVisitor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/codehaus/groovy/classgen/EnumCompletionVisitor.java b/src/main/java/org/codehaus/groovy/classgen/EnumCompletionVisitor.java
index cba687e..8ef3a04 100644
--- a/src/main/java/org/codehaus/groovy/classgen/EnumCompletionVisitor.java
+++ b/src/main/java/org/codehaus/groovy/classgen/EnumCompletionVisitor.java
@@ -37,11 +37,12 @@ import org.codehaus.groovy.ast.stmt.Statement;
 import org.codehaus.groovy.control.CompilationUnit;
 import org.codehaus.groovy.control.SourceUnit;
 import org.codehaus.groovy.transform.TupleConstructorASTTransformation;
-import org.objectweb.asm.Opcodes;
 
 import java.util.ArrayList;
 import java.util.List;
 
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+
 /**
  * Enums have a parent constructor with two arguments from java.lang.Enum.
  * This visitor adds those two arguments into manually created constructors
@@ -85,7 +86,7 @@ public class EnumCompletionVisitor extends ClassCodeVisitorSupport {
                 addMapConstructors(enumClass);
             } else {
                 for (ConstructorNode constructorNode : sctors) {
-                    ConstructorNode init = new ConstructorNode(Opcodes.ACC_PUBLIC, constructorNode.getParameters(), ClassNode.EMPTY_ARRAY, new BlockStatement());
+                    ConstructorNode init = new ConstructorNode(ACC_PUBLIC, constructorNode.getParameters(), ClassNode.EMPTY_ARRAY, new BlockStatement());
                     enumClass.addConstructor(init);
                 }
             }
@@ -144,7 +145,7 @@ public class EnumCompletionVisitor extends ClassCodeVisitorSupport {
     }
 
     private static void addMapConstructors(ClassNode enumClass) {
-        TupleConstructorASTTransformation.addSpecialMapConstructors(enumClass, "One of the enum constants for enum " + enumClass.getName() +
+        TupleConstructorASTTransformation.addSpecialMapConstructors(ACC_PUBLIC, enumClass, "One of the enum constants for enum " + enumClass.getName() +
                 " was initialized with null. Please use a non-null value or define your own constructor.", true);
     }
 

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
index aa995ef..51de450 100644
--- a/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
@@ -51,6 +51,7 @@ import java.util.Map;
 import java.util.Set;
 
 import static org.apache.groovy.ast.tools.ClassNodeUtils.hasNoArgConstructor;
+import static org.apache.groovy.ast.tools.VisibilityUtils.getVisibility;
 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;
@@ -173,9 +174,10 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation i
             ClosureExpression transformed = (ClosureExpression) transformer.transform(post);
             body.addStatement(transformed.getCode());
         }
-        doAddConstructor(cNode, new ConstructorNode(ACC_PUBLIC, params, ClassNode.EMPTY_ARRAY, body));
+        int modifiers = getVisibility(anno, cNode, ConstructorNode.class, ACC_PUBLIC);
+        doAddConstructor(cNode, new ConstructorNode(modifiers, params, ClassNode.EMPTY_ARRAY, body));
         if (noArg && !superList.isEmpty() && !hasNoArgConstructor(cNode)/* && !specialNamedArgCase*/) {
-            createNoArgConstructor(cNode);
+            createNoArgConstructor(cNode, modifiers);
         }
     }
 
@@ -220,9 +222,9 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation i
         }
     }
 
-    private static void createNoArgConstructor(ClassNode cNode) {
+    private static void createNoArgConstructor(ClassNode cNode, int modifiers) {
         Statement body = stmt(ctorX(ClassNode.THIS, args(new MapExpression())));
-        cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body));
+        cNode.addConstructor(new ConstructorNode(modifiers, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body));
     }
 
     private static ClassCodeExpressionTransformer makeMapTypedArgsTransformer() {

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java
index 70053bd..3a7e3ab 100644
--- a/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/NamedVariantASTTransformation.java
@@ -75,7 +75,6 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
     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) {
@@ -121,7 +120,7 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
                     if (getMemberValue(namedParam, "type") == null) {
                         namedParam.addMember("type", classX(fromParam.getType()));
                     }
-                    if (!checkDuplicates(mNode, propNames, name)) return;
+                    if (hasDuplicates(mNode, propNames, name)) return;
                     // TODO check specified type is assignable from declared param type?
                     // ClassNode type = getMemberClassValue(namedParam, "type");
                     if (required) {
@@ -139,7 +138,7 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
                     if (!processDelegateParam(mNode, mapParam, args, propNames, fromParam)) return;
                 } else {
                     args.addExpression(varX(fromParam));
-                    if (!checkDuplicates(mNode, propNames, fromParam.getName())) return;
+                    if (hasDuplicates(mNode, propNames, fromParam.getName())) return;
                     genParams.add(fromParam);
                 }
             }
@@ -163,7 +162,7 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
         }
 
         final BlockStatement body = new BlockStatement();
-        int modifiers = getVisibility(anno, mNode, mNode.getModifiers());
+        int modifiers = getVisibility(anno, mNode, mNode.getClass(), mNode.getModifiers());
         if (mNode instanceof ConstructorNode) {
             body.addStatement(stmt(ctorX(ClassNode.THIS, args)));
             body.addStatement(inner);
@@ -198,7 +197,7 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
         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;
+            if (hasDuplicates(mNode, propNames, next)) return false;
         }
         List<MapEntryExpression> entries = new ArrayList<MapEntryExpression>();
         for (PropertyNode pNode : props) {
@@ -214,12 +213,12 @@ public class NamedVariantASTTransformation extends AbstractASTTransformation {
         return true;
     }
 
-    private boolean checkDuplicates(MethodNode mNode, List<String> propNames, String next) {
+    private boolean hasDuplicates(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;
+            return true;
         }
         propNames.add(next);
-        return true;
+        return false;
     }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
index d225dfb..9696d15 100644
--- a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
@@ -55,6 +55,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import static org.apache.groovy.ast.tools.VisibilityUtils.getVisibility;
 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;
@@ -86,7 +87,6 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
     static final ClassNode MY_TYPE = make(MY_CLASS);
     static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
     private static final ClassNode LHMAP_TYPE = makeWithoutCaching(LinkedHashMap.class, false);
-    private static final ClassNode HMAP_TYPE = makeWithoutCaching(HashMap.class, false);
     private static final ClassNode CHECK_METHOD_TYPE = make(ImmutableASTTransformation.class);
     private static final Class<? extends Annotation> MAP_CONSTRUCTOR_CLASS = MapConstructor.class;
     private static final Map<Class<?>, Expression> primitivesInitialValues;
@@ -128,8 +128,10 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
             List<String> includes = getMemberStringList(anno, "includes");
             boolean allNames = memberHasValue(anno, "allNames", true);
             if (!checkIncludeExcludeUndefinedAware(anno, excludes, includes, MY_TYPE_NAME)) return;
-            if (!checkPropertyList(cNode, includes, "includes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, allProperties, includeSuperFields, false)) return;
-            if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, allProperties, includeSuperFields, false)) return;
+            if (!checkPropertyList(cNode, includes, "includes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, allProperties, includeSuperFields, false))
+                return;
+            if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, allProperties, includeSuperFields, false))
+                return;
             final GroovyClassLoader classLoader = compilationUnit != null ? compilationUnit.getTransformLoader() : source.getClassLoader();
             final PropertyHandler handler = PropertyHandler.createPropertyHandler(this, classLoader, cNode);
             if (handler == null) return;
@@ -251,7 +253,8 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         }
 
         boolean hasMapCons = hasAnnotation(cNode, MapConstructorASTTransformation.MY_TYPE);
-        cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, params.toArray(new Parameter[params.size()]), ClassNode.EMPTY_ARRAY, body));
+        int modifiers = getVisibility(anno, cNode, ConstructorNode.class, ACC_PUBLIC);
+        cNode.addConstructor(new ConstructorNode(modifiers, params.toArray(new Parameter[params.size()]), ClassNode.EMPTY_ARRAY, body));
 
         if (sourceUnit != null && !body.isEmpty()) {
             VariableScopeVisitor scopeVisitor = new VariableScopeVisitor(sourceUnit);
@@ -266,7 +269,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
             ClassNode firstParamType = params.get(0).getType();
             if (params.size() > 1 || firstParamType.equals(ClassHelper.OBJECT_TYPE)) {
                 String message = "The class " + cNode.getName() + " was incorrectly initialized via the map constructor with null.";
-                addSpecialMapConstructors(cNode, message, false);
+                addSpecialMapConstructors(modifiers, cNode, message, false);
             }
         }
     }
@@ -293,7 +296,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         return initialExp;
     }
 
-    public static void addSpecialMapConstructors(ClassNode cNode, String message, boolean addNoArg) {
+    public static void addSpecialMapConstructors(int modifiers, ClassNode cNode, String message, boolean addNoArg) {
         Parameter[] parameters = params(new Parameter(LHMAP_TYPE, "__namedArgs"));
         BlockStatement code = new BlockStatement();
         VariableExpression namedArgs = varX("__namedArgs");
@@ -301,13 +304,13 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         code.addStatement(ifElseS(equalsNullX(namedArgs),
                 illegalArgumentBlock(message),
                 processArgsBlock(cNode, namedArgs)));
-        ConstructorNode init = new ConstructorNode(ACC_PUBLIC, parameters, ClassNode.EMPTY_ARRAY, code);
+        ConstructorNode init = new ConstructorNode(modifiers, parameters, ClassNode.EMPTY_ARRAY, code);
         cNode.addConstructor(init);
         // potentially add a no-arg constructor too
         if (addNoArg) {
             code = new BlockStatement();
             code.addStatement(stmt(ctorX(ClassNode.THIS, ctorX(LHMAP_TYPE))));
-            init = new ConstructorNode(ACC_PUBLIC, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, code);
+            init = new ConstructorNode(modifiers, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, code);
             cNode.addConstructor(init);
         }
     }

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/spec/doc/core-metaprogramming.adoc
----------------------------------------------------------------------
diff --git a/src/spec/doc/core-metaprogramming.adoc b/src/spec/doc/core-metaprogramming.adoc
index 7230e11..888bed9 100644
--- a/src/spec/doc/core-metaprogramming.adoc
+++ b/src/spec/doc/core-metaprogramming.adoc
@@ -1769,6 +1769,13 @@ during class construction. It is ignored by the main Groovy compiler but is refe
 like `@TupleConstructor`, `@MapConstructor`, and `@ImmutableBase`. It is frequently used behind the
 scenes by the `@Immutable` meta-annotation.
 
+[[xform-VisibilityOptions]]
+===== `@groovy.transform.VisibilityOptions`
+
+This annotation allows you to specify a custom visibility for a construct generated by another transformation.
+It is ignored by the main Groovy compiler but is referenced by other transformations
+like `@TupleConstructor`, `@MapConstructor`, and `@NamedVariant`.
+
 [[xform-ImumtableOptions]]
 ===== `@groovy.transform.ImmutableOptions`
 

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/spec/test/CodeGenerationASTTransformsTest.groovy
----------------------------------------------------------------------
diff --git a/src/spec/test/CodeGenerationASTTransformsTest.groovy b/src/spec/test/CodeGenerationASTTransformsTest.groovy
index f2bb2b7..efc73ae 100644
--- a/src/spec/test/CodeGenerationASTTransformsTest.groovy
+++ b/src/spec/test/CodeGenerationASTTransformsTest.groovy
@@ -1434,15 +1434,20 @@ firstLastAge()
 // tag::builder_initializer_immutable[]
 import groovy.transform.builder.*
 import groovy.transform.*
+import static groovy.transform.options.Visibility.PRIVATE
 
 @Builder(builderStrategy=InitializerStrategy)
 @Immutable
+@VisibilityOptions(PRIVATE)
 class Person {
     String first
     String last
     int born
 }
 
+def publicCons = Person.constructors
+assert publicCons.size() == 1
+
 @CompileStatic
 def createFirstLastBorn() {
   def p = new Person(Person.createInitializer().first('Johnny').last('Depp').born(1963))

http://git-wip-us.apache.org/repos/asf/groovy/blob/a98efdb3/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy b/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
index 991cdb4..71050ff 100644
--- a/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
+++ b/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
@@ -214,6 +214,69 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
         '''
     }
 
+    // GROOVY-7981
+    void testVisibilityOptions() {
+        assertScript '''
+            import groovy.transform.*
+            import static groovy.transform.options.Visibility.*
+            import static java.lang.reflect.Modifier.isPrivate
+            import static org.codehaus.groovy.control.CompilePhase.*
+
+            @VisibilityOptions(PRIVATE)
+            @Immutable
+            @ASTTest(phase = CANONICALIZATION,
+                     value = {
+                         node.constructors.every { isPrivate(it.modifiers) }
+                     })
+            class Person {
+                String first, last
+                int age
+                static makePerson(Map args) {
+                    new Person(args)
+                }
+            }
+
+            @CompileStatic
+            def method() {
+                def p = Person.makePerson(first: 'John', last: 'Smith', age: 20)
+                assert p.toString() == 'Person(John, Smith, 20)'
+            }
+            method()
+        '''
+    }
+
+    // GROOVY-7981
+    void testMultipleVisibilityOptions() {
+        assertScript '''
+            import groovy.transform.*
+            import java.lang.reflect.Modifier
+            import static groovy.transform.options.Visibility.*
+            import static org.codehaus.groovy.control.CompilePhase.*
+
+            @VisibilityOptions(value = PROTECTED, id = 'first_only')
+            @VisibilityOptions(constructor = PRIVATE, id = 'age_only')
+            @TupleConstructor(visibilityId = 'first_only', includes = 'first', defaults = false, force = true)
+            @TupleConstructor(visibilityId = 'age_only', includes = 'age', defaults = false, force = true)
+            @ASTTest(phase = CANONICALIZATION,
+                     value = {
+                         assert node.constructors.size() == 2
+                         node.constructors.each {
+                             assert (it.typeDescriptor == 'void <init>(java.lang.String)' && it.modifiers == Modifier.PROTECTED) ||
+                             (it.typeDescriptor == 'void <init>(int)' && it.modifiers == Modifier.PRIVATE)
+                         }
+                     })
+            class Person {
+                String first, last
+                int age
+                static test() {
+                    assert new Person('John').first == 'John'
+                    assert new Person(42).age == 42
+                }
+            }
+            Person.test()
+        '''
+    }
+
     // GROOVY-8455, GROOVY-8453
     void testPropPsuedoPropAndFieldOrderIncludingInheritedMembers() {
         assertScript '''