You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by em...@apache.org on 2022/11/21 20:28:31 UTC

[groovy] branch GROOVY_4_0_X updated: GROOVY-10790: `@MapConstructor(noArg=true)` plus `@TupleConstructor`

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

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


The following commit(s) were added to refs/heads/GROOVY_4_0_X by this push:
     new 95c7e6ba02 GROOVY-10790: `@MapConstructor(noArg=true)` plus `@TupleConstructor`
95c7e6ba02 is described below

commit 95c7e6ba025a1846c7638dd4f25c5c141206fccb
Author: Eric Milles <er...@thomsonreuters.com>
AuthorDate: Mon Nov 21 11:04:21 2022 -0600

    GROOVY-10790: `@MapConstructor(noArg=true)` plus `@TupleConstructor`
---
 .../java/org/codehaus/groovy/ast/Parameter.java    |   4 +-
 .../org/codehaus/groovy/classgen/Verifier.java     |  30 +-
 .../org/codehaus/groovy/control/SourceUnit.java    |  26 +-
 .../codehaus/groovy/control/messages/Message.java  |  41 +-
 .../groovy/control/messages/SimpleMessage.java     |  30 +-
 .../groovy/control/messages/WarningMessage.java    |  38 +-
 .../groovy/tools/javac/JavaStubGenerator.java      |   4 +-
 .../groovy/transform/ASTTransformationVisitor.java |   7 +-
 .../transform/MapConstructorASTTransformation.java |  23 +-
 .../TupleConstructorASTTransformation.java         | 146 +++----
 .../groovy/transform/ImmutableTransformTest.groovy | 467 +++++++++------------
 .../transform/TupleConstructorTransformTest.groovy | 308 ++++++++------
 12 files changed, 555 insertions(+), 569 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/ast/Parameter.java b/src/main/java/org/codehaus/groovy/ast/Parameter.java
index 5c25900f2a..93f0fde79f 100644
--- a/src/main/java/org/codehaus/groovy/ast/Parameter.java
+++ b/src/main/java/org/codehaus/groovy/ast/Parameter.java
@@ -85,9 +85,9 @@ public class Parameter extends AnnotatedNode implements Variable {
         return defaultValue;
     }
 
-    public void setInitialExpression(Expression init) {
+    public void setInitialExpression(final Expression init) {
         defaultValue = init;
-        hasDefaultValue = defaultValue != null;
+        hasDefaultValue = (init != null);
     }
 
     @Override
diff --git a/src/main/java/org/codehaus/groovy/classgen/Verifier.java b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
index f431ea8403..ecee1e4a6d 100644
--- a/src/main/java/org/codehaus/groovy/classgen/Verifier.java
+++ b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
@@ -93,6 +93,7 @@ import static java.lang.reflect.Modifier.isPublic;
 import static java.lang.reflect.Modifier.isStatic;
 import static java.util.stream.Collectors.joining;
 import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.hasAnnotation;
+import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.isGenerated;
 import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsGenerated;
 import static org.apache.groovy.ast.tools.ConstructorNodeUtils.getFirstIfSpecialConstructorCall;
 import static org.apache.groovy.ast.tools.ExpressionUtils.transformInlineConstants;
@@ -109,6 +110,7 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorThisX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.fieldX;
@@ -987,7 +989,6 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
         List<ConstructorNode> constructors = new ArrayList<>(type.getDeclaredConstructors());
         addDefaultParameters(constructors, (arguments, params, method) -> {
             // GROOVY-9151: check for references to parameters that have been removed
-            List<Parameter> paramList = Arrays.asList(params);
             for (ListIterator<Expression> it = arguments.getExpressions().listIterator(); it.hasNext(); ) {
                 Expression argument = it.next();
                 if (argument instanceof CastExpression) {
@@ -997,9 +998,8 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
                     VariableExpression v = (VariableExpression) argument;
                     if (v.getAccessedVariable() instanceof Parameter) {
                         Parameter p = (Parameter) v.getAccessedVariable();
-                        if (p.hasInitialExpression() && !paramList.contains(p)
-                                && p.getInitialExpression() instanceof ConstantExpression) {
-                            // replace argument "(Type) param" with "(Type) <param's default>" for simple default value
+                        if (p.getInitialExpression() instanceof ConstantExpression && !Arrays.asList(params).contains(p)){
+                            // for simple default value, replace argument "(Type) param" with "(Type) <<param's default>>"
                             it.set(castX(method.getParameters()[it.nextIndex() - 1].getType(), p.getInitialExpression()));
                         }
                     }
@@ -1010,7 +1010,7 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
                 public void visitVariableExpression(final VariableExpression e) {
                     if (e.getAccessedVariable() instanceof Parameter) {
                         Parameter p = (Parameter) e.getAccessedVariable();
-                        if (p.hasInitialExpression() && !paramList.contains(p)) {
+                        if (p.hasInitialExpression() && !Arrays.asList(params).contains(p)) {
                             String error = String.format(
                                     "The generated constructor \"%s(%s)\" references parameter '%s' which has been replaced by a default value expression.",
                                     type.getNameWithoutPackage(),
@@ -1023,9 +1023,15 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
             };
             visitor.visitArgumentlistExpression(arguments);
 
-            // delegate to original constructor using arguments derived from defaults
-            Statement code = new ExpressionStatement(new ConstructorCallExpression(ClassNode.THIS, arguments));
-            addConstructor(params, (ConstructorNode) method, code, type);
+            ConstructorNode old = type.getDeclaredConstructor(params);
+            if (old == null || isGenerated(old)) { type.removeConstructor(old);
+                // delegate to original constructor using arguments derived from defaults
+                addConstructor(params, (ConstructorNode) method, stmt(ctorThisX(arguments)), type);
+            } else {
+                String warning = "Default argument(s) specify duplicate constructor: " +
+                        old.getTypeDescriptor().replace("void <init>", type.getNameWithoutPackage());
+                type.getModule().getContext().addWarning(warning, method.getLineNumber() > 0 ? method : type);
+            }
         });
     }
 
@@ -1088,10 +1094,10 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
         }
 
         for (Parameter parameter : parameters) {
-            if (parameter.hasInitialExpression()) {
-                // remove default expression and store it as node metadata
-                parameter.putNodeMetaData(Verifier.INITIAL_EXPRESSION,
-                        parameter.getInitialExpression());
+            Expression value = parameter.getInitialExpression();
+            if (value != null) {
+                // move the default expression from parameter to node metadata
+                parameter.putNodeMetaData(Verifier.INITIAL_EXPRESSION, value);
                 parameter.setInitialExpression(null);
             }
         }
diff --git a/src/main/java/org/codehaus/groovy/control/SourceUnit.java b/src/main/java/org/codehaus/groovy/control/SourceUnit.java
index b5ac3d492b..fb6c35e167 100644
--- a/src/main/java/org/codehaus/groovy/control/SourceUnit.java
+++ b/src/main/java/org/codehaus/groovy/control/SourceUnit.java
@@ -29,8 +29,10 @@ import org.codehaus.groovy.control.io.URLReaderSource;
 import org.codehaus.groovy.control.messages.Message;
 import org.codehaus.groovy.control.messages.SimpleMessage;
 import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
+import org.codehaus.groovy.control.messages.WarningMessage;
 import org.codehaus.groovy.syntax.Reduction;
 import org.codehaus.groovy.syntax.SyntaxException;
+import org.codehaus.groovy.syntax.Token;
 import org.codehaus.groovy.tools.Utilities;
 
 import java.io.File;
@@ -265,7 +267,7 @@ public class SourceUnit extends ProcessingUnit {
         return this.ast;
     }
 
-    //---------------------------------------------------------------------------
+    //--------------------------------------------------------------------------
     // SOURCE SAMPLING
 
     /**
@@ -306,7 +308,7 @@ public class SourceUnit extends ProcessingUnit {
      * @param e the exception that occurred
      * @throws CompilationFailedException on error
      */
-    public void addException(Exception e) throws CompilationFailedException {
+    public void addException(final Exception e) throws CompilationFailedException {
         getErrorCollector().addException(e, this);
     }
 
@@ -319,24 +321,32 @@ public class SourceUnit extends ProcessingUnit {
      * @param se the exception, which should have line and column information
      * @throws CompilationFailedException on error
      */
-    public void addError(SyntaxException se) throws CompilationFailedException {
+    public void addError(final SyntaxException se) throws CompilationFailedException {
         getErrorCollector().addError(se, this);
     }
 
     /**
      * Convenience wrapper for {@link ErrorCollector#addFatalError(org.codehaus.groovy.control.messages.Message)}.
      *
-     * @param msg the error message
-     * @param node the AST node
+     * @param text the error message
+     * @param node for locating the offending code
      * @throws CompilationFailedException on error
      *
      * @since 3.0.0
      */
-    public void addFatalError(String msg, ASTNode node) throws CompilationFailedException {
-        getErrorCollector().addFatalError(Message.create(new SyntaxException(msg, node), this));
+    public void addFatalError(final String text, final ASTNode node) throws CompilationFailedException {
+        getErrorCollector().addFatalError(Message.create(new SyntaxException(text, node), this));
+    }
+
+    /**
+     * @since 4.0.7
+     */
+    public void addWarning(final String text, final ASTNode node) {
+        Token token = new Token(0, "", node.getLineNumber(), node.getColumnNumber()); // ASTNode to CSTNode
+        getErrorCollector().addWarning(new WarningMessage(WarningMessage.POSSIBLE_ERRORS, text, token, this));
     }
 
-    public void addErrorAndContinue(SyntaxException se) {
+    public void addErrorAndContinue(final SyntaxException se) {
         getErrorCollector().addErrorAndContinue(se, this);
     }
 
diff --git a/src/main/java/org/codehaus/groovy/control/messages/Message.java b/src/main/java/org/codehaus/groovy/control/messages/Message.java
index 2392607a9e..f72fd2c048 100644
--- a/src/main/java/org/codehaus/groovy/control/messages/Message.java
+++ b/src/main/java/org/codehaus/groovy/control/messages/Message.java
@@ -31,44 +31,37 @@ import java.io.PrintWriter;
 public abstract class Message {
 
     /**
-     * Writes the message to the specified PrintWriter.  The supplied
-     * ProcessingUnit is the unit that holds this Message.
+     * Creates a new {@code Message} from the specified text.
      */
-    public abstract void write(PrintWriter writer, Janitor janitor);
+    public static Message create(final String text, final ProcessingUnit owner) {
+        return new SimpleMessage(text, owner);
+    }
 
     /**
-     * A synonym for write( writer, owner, null ).
+     * Creates a new {@code Message} from the specified text and data.
      */
-    public final void write(PrintWriter writer) {
-        write(writer, null);
+    public static Message create(final String text, final Object data, final ProcessingUnit owner) {
+        return new SimpleMessage(text, data, owner);
     }
 
-    //---------------------------------------------------------------------------
-    // FACTORY METHODS
-
     /**
-     * Creates a new Message from the specified text.
+     * Creates a new {@code Message} from the specified {@code SyntaxException}.
      */
-    public static Message create(String text, ProcessingUnit owner) {
-        return new SimpleMessage(text, owner);
+    public static Message create(final SyntaxException error, final SourceUnit owner) {
+        return new SyntaxErrorMessage(error, owner);
     }
 
+    //--------------------------------------------------------------------------
+
     /**
-     * Creates a new Message from the specified text.
+     * Writes this message to the specified {@link PrintWriter}.
      */
-    public static Message create(String text, Object data, ProcessingUnit owner) {
-        return new SimpleMessage(text, data, owner);
-    }
+    public abstract void write(PrintWriter writer, Janitor janitor);
 
     /**
-     * Creates a new Message from the specified SyntaxException.
+     * Writes this message to the specified {@link PrintWriter}.
      */
-    public static Message create(SyntaxException error, SourceUnit owner) {
-        return new SyntaxErrorMessage(error, owner);
+    public final void write(final PrintWriter writer) {
+        write(writer, null);
     }
-
 }
-
-
-
-
diff --git a/src/main/java/org/codehaus/groovy/control/messages/SimpleMessage.java b/src/main/java/org/codehaus/groovy/control/messages/SimpleMessage.java
index 5afc7687e7..2c537bc5c8 100644
--- a/src/main/java/org/codehaus/groovy/control/messages/SimpleMessage.java
+++ b/src/main/java/org/codehaus/groovy/control/messages/SimpleMessage.java
@@ -28,32 +28,32 @@ import java.io.PrintWriter;
  * A base class for compilation messages.
  */
 public class SimpleMessage extends Message {
-    protected String message;  // Message text
-    protected Object data;     // Data, when the message text is an I18N identifier
+
+    /** used when {@link message} is an I18N identifier */
+    protected Object data;
+    protected String message;
     protected ProcessingUnit owner;
 
-    public SimpleMessage(String message, ProcessingUnit source) {
-        this(message, null, source);
+    public SimpleMessage(final String message, final ProcessingUnit owner) {
+        this(message, null, owner);
     }
 
-    public SimpleMessage(String message, Object data, ProcessingUnit source) {
+    public SimpleMessage(final String message, final Object data, final ProcessingUnit owner) {
         this.message = message;
-        this.data = null;
-        this.owner = source;
+        this.owner = owner;
+        this.data = data;
+    }
+
+    public String getMessage() {
+        return message;
     }
 
     @Override
-    public void write(PrintWriter writer, Janitor janitor) {
+    public void write(final PrintWriter writer, final Janitor janitor) {
         if (owner instanceof SourceUnit) {
-            String name = ((SourceUnit) owner).getName();
-            writer.println("" + name + ": " + message);
+            writer.println(((SourceUnit) owner).getName() + ": " + message);
         } else {
             writer.println(message);
         }
     }
-
-    public String getMessage() {
-        return message;
-    }
-
 }
diff --git a/src/main/java/org/codehaus/groovy/control/messages/WarningMessage.java b/src/main/java/org/codehaus/groovy/control/messages/WarningMessage.java
index d28d800488..d17c2aeaec 100644
--- a/src/main/java/org/codehaus/groovy/control/messages/WarningMessage.java
+++ b/src/main/java/org/codehaus/groovy/control/messages/WarningMessage.java
@@ -28,18 +28,23 @@ import java.io.PrintWriter;
  * A class for warning messages.
  */
 public class WarningMessage extends LocatedMessage {
-    //---------------------------------------------------------------------------
+
+    //--------------------------------------------------------------------------
     // WARNING LEVELS
 
-    public static final int NONE = 0;  // For querying, ignore all errors
-    public static final int LIKELY_ERRORS = 1;  // Warning indicates likely error
-    public static final int POSSIBLE_ERRORS = 2;  // Warning indicates possible error
-    public static final int PARANOIA = 3;  // Warning indicates paranoia on the part of the compiler
+    /** Ignore all (for querying) */
+    public static final int NONE = 0;
+    /** Warning indicates likely error */
+    public static final int LIKELY_ERRORS = 1;
+    /** Warning indicates possible error */
+    public static final int POSSIBLE_ERRORS = 2;
+    /** Warning indicates paranoia on the part of the compiler */
+    public static final int PARANOIA = 3;
 
     /**
      * Returns true if a warning would be relevant to the specified level.
      */
-    public static boolean isRelevant(int actual, int limit) {
+    public static boolean isRelevant(final int actual, final int limit) {
         return actual <= limit;
     }
 
@@ -47,23 +52,23 @@ public class WarningMessage extends LocatedMessage {
      * Returns true if this message is as or more important than the
      * specified importance level.
      */
-    public boolean isRelevant(int importance) {
+    public boolean isRelevant(final int importance) {
         return isRelevant(this.importance, importance);
     }
 
-    //---------------------------------------------------------------------------
-    // CONSTRUCTION AND DATA ACCESS
+    //--------------------------------------------------------------------------
 
-    private final int importance;  // The warning level, for filtering
+    /** The warning level (for filtering). */
+    private final int importance;
 
     /**
      * Creates a new warning message.
      *
      * @param importance the warning level
      * @param message    the message text
-     * @param context    context information for locating the offending source text
+     * @param context    for locating the offending source text
      */
-    public WarningMessage(int importance, String message, CSTNode context, SourceUnit owner) {
+    public WarningMessage(final int importance, final String message, final CSTNode context, final SourceUnit owner) {
         super(message, context, owner);
         this.importance = importance;
     }
@@ -73,18 +78,17 @@ public class WarningMessage extends LocatedMessage {
      *
      * @param importance the warning level
      * @param message    the message text
-     * @param data       additional data needed when generating the message
-     * @param context    context information for locating the offending source text
+     * @param data       data needed for generating the message
+     * @param context    for locating the offending source text
      */
-    public WarningMessage(int importance, String message, Object data, CSTNode context, SourceUnit owner) {
+    public WarningMessage(final int importance, final String message, final Object data, final CSTNode context, final SourceUnit owner) {
         super(message, data, context, owner);
         this.importance = importance;
     }
 
     @Override
-    public void write(PrintWriter writer, Janitor janitor) {
+    public void write(final PrintWriter writer, final Janitor janitor) {
         writer.print("warning: ");
         super.write(writer, janitor);
     }
-
 }
diff --git a/src/main/java/org/codehaus/groovy/tools/javac/JavaStubGenerator.java b/src/main/java/org/codehaus/groovy/tools/javac/JavaStubGenerator.java
index b5fa76ae70..83f6f7025f 100644
--- a/src/main/java/org/codehaus/groovy/tools/javac/JavaStubGenerator.java
+++ b/src/main/java/org/codehaus/groovy/tools/javac/JavaStubGenerator.java
@@ -79,6 +79,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.stream.Stream;
 
+import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsGenerated;
 import static org.apache.groovy.ast.tools.ConstructorNodeUtils.getFirstIfSpecialConstructorCall;
 import static org.codehaus.groovy.ast.ClassHelper.CLASS_Type;
 import static org.codehaus.groovy.ast.ClassHelper.getUnwrapper;
@@ -280,13 +281,14 @@ public class JavaStubGenerator {
 
                 @Override
                 protected void addConstructor(Parameter[] params, ConstructorNode ctor, Statement code, ClassNode node) {
-                    if (code instanceof ExpressionStatement) { //GROOVY-4508
+                    if (!(code instanceof BlockStatement)) { // GROOVY-4508
                         Statement stmt = code;
                         code = new BlockStatement();
                         ((BlockStatement) code).addStatement(stmt);
                     }
                     ConstructorNode newCtor = new ConstructorNode(ctor.getModifiers(), params, ctor.getExceptions(), code);
                     newCtor.setDeclaringClass(node);
+                    markAsGenerated(node, newCtor);
                     constructors.add(newCtor);
                 }
 
diff --git a/src/main/java/org/codehaus/groovy/transform/ASTTransformationVisitor.java b/src/main/java/org/codehaus/groovy/transform/ASTTransformationVisitor.java
index 499a7562d1..137ae5f815 100644
--- a/src/main/java/org/codehaus/groovy/transform/ASTTransformationVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/ASTTransformationVisitor.java
@@ -352,21 +352,20 @@ public final class ASTTransformationVisitor extends ClassCodeVisitorSupport {
         }
     }
 
-    private static void addPhaseOperationsForGlobalTransforms(CompilationUnit compilationUnit,
-            Map<String, URL> transformNames, boolean isFirstScan) {
+    private static void addPhaseOperationsForGlobalTransforms(CompilationUnit compilationUnit, Map<String, URL> transformNames, boolean isFirstScan) {
         GroovyClassLoader transformLoader = compilationUnit.getTransformLoader();
         for (Map.Entry<String, URL> entry : transformNames.entrySet()) {
             try {
                 Class<?> gTransClass = transformLoader.loadClass(entry.getKey(), false, true, false);
                 GroovyASTTransformation transformAnnotation = gTransClass.getAnnotation(GroovyASTTransformation.class);
                 if (transformAnnotation == null) {
-                    compilationUnit.getErrorCollector().addWarning(new WarningMessage(
+                    compilationUnit.getErrorCollector().addWarning(
                         WarningMessage.POSSIBLE_ERRORS,
                         "Transform Class " + entry.getKey() + " is specified as a global transform in " + entry.getValue().toExternalForm()
                         + " but it is not annotated by " + GroovyASTTransformation.class.getName()
                         + " the global transform associated with it may fail and cause the compilation to fail.",
                         null,
-                        null));
+                        null);
                     continue;
                 }
                 if (ASTTransformation.class.isAssignableFrom(gTransClass)) {
diff --git a/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
index dbc31c467b..40538689bb 100644
--- a/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
@@ -27,6 +27,7 @@ import org.codehaus.groovy.ast.AnnotatedNode;
 import org.codehaus.groovy.ast.AnnotationNode;
 import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
 import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
+import org.codehaus.groovy.ast.ClassHelper;
 import org.codehaus.groovy.ast.ClassNode;
 import org.codehaus.groovy.ast.ConstructorNode;
 import org.codehaus.groovy.ast.DynamicVariable;
@@ -53,8 +54,6 @@ import java.util.Set;
 import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsGenerated;
 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;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.copyStatementsWithSuperAdjustment;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
@@ -73,11 +72,11 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation i
 
     private CompilationUnit compilationUnit;
 
-    static final Class MY_CLASS = MapConstructor.class;
-    static final ClassNode MY_TYPE = make(MY_CLASS);
+    static final Class<?> MY_CLASS = MapConstructor.class;
+    static final ClassNode MY_TYPE = ClassHelper.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 LHMAP_TYPE = makeWithoutCaching(LinkedHashMap.class, false);
+    private static final ClassNode MAP_TYPE = ClassHelper.makeWithoutCaching(Map.class, false);
+    private static final ClassNode LHMAP_TYPE = ClassHelper.makeWithoutCaching(LinkedHashMap.class, false);
 
     @Override
     public String getAnnotationName() {
@@ -85,7 +84,12 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation i
     }
 
     @Override
-    public void visit(ASTNode[] nodes, SourceUnit source) {
+    public void setCompilationUnit(final CompilationUnit unit) {
+        this.compilationUnit = unit;
+    }
+
+    @Override
+    public void visit(final ASTNode[] nodes, final SourceUnit source) {
         init(nodes, source);
         AnnotatedNode parent = (AnnotatedNode) nodes[1];
         AnnotationNode anno = (AnnotationNode) nodes[0];
@@ -256,9 +260,4 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation i
             }
         };
     }
-
-    @Override
-    public void setCompilationUnit(CompilationUnit unit) {
-        this.compilationUnit = unit;
-    }
 }
diff --git a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
index 8762b5d867..87bd6c5c26 100644
--- a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
@@ -45,7 +45,6 @@ import org.codehaus.groovy.ast.expr.PropertyExpression;
 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.VariableScopeVisitor;
 import org.codehaus.groovy.control.CompilationUnit;
@@ -61,7 +60,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import static groovy.transform.DefaultsMode.AUTO;
 import static groovy.transform.DefaultsMode.OFF;
 import static groovy.transform.DefaultsMode.ON;
 import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedConstructor;
@@ -70,7 +68,6 @@ import static org.apache.groovy.ast.tools.ConstructorNodeUtils.checkPropNamesS;
 import static org.apache.groovy.ast.tools.VisibilityUtils.getVisibility;
 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.block;
 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.copyStatementsWithSuperAdjustment;
@@ -86,8 +83,10 @@ 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.throwS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
+import static org.codehaus.groovy.runtime.DefaultGroovyMethods.plus;
 import static org.codehaus.groovy.transform.ImmutableASTTransformation.makeImmutable;
 import static org.codehaus.groovy.transform.NamedVariantASTTransformation.processImplicitNamedParam;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
 
 /**
  * Handles generation of code for the @TupleConstructor annotation.
@@ -105,6 +104,11 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
     private static final ClassNode LHMAP_TYPE = ClassHelper.makeWithoutCaching(LinkedHashMap.class, false);
     private static final ClassNode POJO_TYPE = ClassHelper.make(POJO.class);
 
+    @Override
+    public int priority() {
+        return 5;
+    }
+
     @Override
     public String getAnnotationName() {
         return MY_TYPE_NAME;
@@ -171,18 +175,19 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
                                           final boolean includeProperties, final boolean includeSuperFields, final boolean includeSuperProperties,
                                           final List<String> excludes, final List<String> includes, final boolean allNames, final boolean allProperties,
                                           final SourceUnit sourceUnit, final PropertyHandler handler, final ClosureExpression pre, final ClosureExpression post) {
-        boolean callSuper = xform.memberHasValue(anno, "callSuper", true);
-        boolean force = xform.memberHasValue(anno, "force", true);
+        boolean namedVariant = xform.memberHasValue(anno, "namedVariant", Boolean.TRUE);
+        boolean callSuper = xform.memberHasValue(anno, "callSuper", Boolean.TRUE);
         DefaultsMode defaultsMode = maybeDefaultsMode(anno, "defaultsMode");
         if (defaultsMode == null) {
-            if (anno.getMember("defaults") == null) {
-                defaultsMode = ON;
-            } else {
-                boolean defaults = !xform.memberHasValue(anno, "defaults", false);
-                defaultsMode = defaults ? ON : OFF;
-            }
+            boolean defaults = anno.getMember("defaults") == null
+                    || !xform.memberHasValue(anno, "defaults", Boolean.FALSE);
+            defaultsMode = defaults ? ON : OFF;
         }
-        boolean namedVariant = xform.memberHasValue(anno, "namedVariant", true);
+        boolean force = xform.memberHasValue(anno, "force", Boolean.TRUE);
+        boolean makeImmutable = makeImmutable(cNode);
+
+        // no processing if explicit constructor(s) found, unless forced or ImmutableBase is in play
+        if (!force && !makeImmutable && hasExplicitConstructor(null, cNode)) return;
 
         Set<String> names = new HashSet<>();
         List<PropertyNode> superList;
@@ -191,16 +196,8 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         } else {
             superList = new ArrayList<>();
         }
-
         List<PropertyNode> list = getAllProperties(names, cNode, includeProperties, includeFields, false, allProperties, false, true);
 
-        boolean makeImmutable = makeImmutable(cNode);
-        boolean specialNamedArgCase = (ImmutableASTTransformation.isSpecialNamedArgCase(list, defaultsMode == OFF) && superList.isEmpty()) ||
-                (ImmutableASTTransformation.isSpecialNamedArgCase(superList, defaultsMode == OFF) && list.isEmpty());
-
-        // no processing if existing constructors found unless forced or ImmutableBase in play
-        if (hasExplicitConstructor(null, cNode) && !force && !makeImmutable) return;
-
         List<Parameter> params = new ArrayList<>();
         List<Expression> superParams = new ArrayList<>();
         BlockStatement preBody = new BlockStatement();
@@ -215,23 +212,25 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
 
         BlockStatement body = new BlockStatement();
 
-        List<PropertyNode> tempList = new ArrayList<>(list);
-        tempList.addAll(superList);
-        if (!handler.validateProperties(xform, body, cNode, tempList)) {
+        if (!handler.validateProperties(xform, body, cNode, plus(list, superList))) {
             return;
         }
 
+        boolean specialNamedArgCase = (superList.isEmpty() && ImmutableASTTransformation.isSpecialNamedArgCase(list, defaultsMode == OFF))
+                || (list.isEmpty() && ImmutableASTTransformation.isSpecialNamedArgCase(superList, defaultsMode == OFF));
+
         for (PropertyNode pNode : superList) {
             String name = pNode.getName();
             FieldNode fNode = pNode.getField();
-            if (shouldSkipUndefinedAware(name, excludes, includes, allNames)) continue;
-            params.add(createParam(fNode, name, defaultsMode, xform, makeImmutable));
-            if (callSuper) {
-                superParams.add(varX(name));
-            } else if (!superInPre && !specialNamedArgCase) {
-                Statement propInit = handler.createPropInit(xform, anno, cNode, pNode, null);
-                if (propInit != null) {
-                    body.addStatement(propInit);
+            if (!shouldSkipUndefinedAware(name, excludes, includes, allNames)) {
+                params.add(createParam(fNode, name, defaultsMode, xform, makeImmutable));
+                if (callSuper) {
+                    superParams.add(varX(name));
+                } else if (!superInPre && !specialNamedArgCase) {
+                    Statement propInit = handler.createPropInit(xform, anno, cNode, pNode, null);
+                    if (propInit != null) {
+                        body.addStatement(propInit);
+                    }
                 }
             }
         }
@@ -256,8 +255,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         }
 
         if (includes != null) {
-            Comparator<Parameter> includeComparator = Comparator.comparingInt(p -> includes.indexOf(p.getName()));
-            params.sort(includeComparator);
+            params.sort(Comparator.comparingInt(p -> includes.indexOf(p.getName())));
         }
 
         for (PropertyNode pNode : list) {
@@ -273,12 +271,12 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
             body.addStatement(post.getCode());
         }
 
-        boolean hasMapCons = AnnotatedNodeUtils.hasAnnotation(cNode, MapConstructorASTTransformation.MY_TYPE);
-        int modifiers = getVisibility(anno, cNode, ConstructorNode.class, org.objectweb.asm.Opcodes.ACC_PUBLIC);
-        ConstructorNode consNode = addGeneratedConstructor(cNode, modifiers, params.toArray(Parameter.EMPTY_ARRAY), ClassNode.EMPTY_ARRAY, body);
+        int modifiers = getVisibility(anno, cNode, ConstructorNode.class, ACC_PUBLIC);
+        // add main tuple constructor; if any parameters have default values then Verifier will generate the other variants
+        ConstructorNode tupleCtor = addGeneratedConstructor(cNode, modifiers, params.toArray(Parameter.EMPTY_ARRAY), ClassNode.EMPTY_ARRAY, body);
         if (cNode.getNodeMetaData("_RECORD_HEADER") != null) {
-            consNode.addAnnotations(cNode.getAnnotations());
-            consNode.putNodeMetaData("_SKIPPABLE_ANNOTATIONS", Boolean.TRUE);
+            tupleCtor.addAnnotations(cNode.getAnnotations());
+            tupleCtor.putNodeMetaData("_SKIPPABLE_ANNOTATIONS", Boolean.TRUE);
         }
         if (namedVariant) {
             BlockStatement inner = new BlockStatement();
@@ -289,26 +287,25 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
             List<String> propNames = new ArrayList<>();
             Map<Parameter, Expression> seen = new HashMap<>();
             for (Parameter p : params) {
-                if (!processImplicitNamedParam(xform, consNode, mapParam, inner, args, propNames, p, false, seen)) return;
+                if (!processImplicitNamedParam(xform, tupleCtor, mapParam, inner, args, propNames, p, false, seen)) return;
             }
-            NamedVariantASTTransformation.createMapVariant(xform, consNode, anno, mapParam, genParams, cNode, inner, args, propNames);
+            NamedVariantASTTransformation.createMapVariant(xform, tupleCtor, anno, mapParam, genParams, cNode, inner, args, propNames);
         }
 
         if (sourceUnit != null && !body.isEmpty()) {
-            VariableScopeVisitor scopeVisitor = new VariableScopeVisitor(sourceUnit);
-            scopeVisitor.visitClass(cNode);
+            new VariableScopeVisitor(sourceUnit).visitClass(cNode);
         }
 
-        // GROOVY-8868 don't want an empty body to cause the constructor to be deleted later
-        if (body.isEmpty()) {
-            body.addStatement(new ExpressionStatement(ConstantExpression.EMPTY_EXPRESSION));
+        if (body.isEmpty()) { // GROOVY-8868: retain empty constructor
+            body.addStatement(stmt(ConstantExpression.EMPTY_EXPRESSION));
         }
 
         // If the first param is def or a Map, named args might not work as expected so we add a hard-coded map constructor in this case
         // we don't do it for LinkedHashMap for now (would lead to duplicate signature)
         // or if there is only one Map property (for backwards compatibility)
         // or if there is already a @MapConstructor annotation
-        if (!params.isEmpty() && defaultsMode != OFF && !hasMapCons && specialNamedArgCase) {
+        if (!params.isEmpty() && defaultsMode != OFF && specialNamedArgCase
+                && !AnnotatedNodeUtils.hasAnnotation(cNode, MapConstructorASTTransformation.MY_TYPE)) {
             ClassNode firstParamType = params.get(0).getType();
             if (params.size() > 1 || ClassHelper.isObjectType(firstParamType)) {
                 String message = "The class " + cNode.getName() + " was incorrectly initialized via the map constructor with null.";
@@ -317,31 +314,31 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         }
     }
 
-    private static Parameter createParam(FieldNode fNode, String name, DefaultsMode defaultsMode, AbstractASTTransformation xform, boolean makeImmutable) {
+    private static Parameter createParam(final FieldNode fNode, final String name, final DefaultsMode defaultsMode, final AbstractASTTransformation xform, final boolean makeImmutable) {
         ClassNode fType = fNode.getType();
         ClassNode type = fType.getPlainNodeReference();
-        type.setGenericsPlaceHolder(fType.isGenericsPlaceHolder());
         type.setGenericsTypes(fType.getGenericsTypes());
-        Parameter param = new Parameter(type, name);
-        if (defaultsMode == ON) {
-            param.setInitialExpression(providedOrDefaultInitialValue(fNode));
-        } else if (defaultsMode == AUTO && fNode.hasInitialExpression()) {
-            param.setInitialExpression(fNode.getInitialExpression());
-            fNode.setInitialValueExpression(null);
-        } else if (!makeImmutable && fNode.hasInitialExpression()) {
-            xform.addError("Error during " + MY_TYPE_NAME + " processing, default value processing disabled but default value found for '" + fNode.getName() + "'", fNode);
-        }
-        return param;
-    }
+        type.setGenericsPlaceHolder(fType.isGenericsPlaceHolder());
 
-    private static Expression providedOrDefaultInitialValue(final FieldNode fNode) {
-        ClassNode fType = fNode.getType();
         Expression init = fNode.getInitialExpression();
-        fNode.setInitialValueExpression(null); // GROOVY-10238
-        if (init == null || (ClassHelper.isPrimitiveType(fType) && ExpressionUtils.isNullConstant(init))) {
-            init = defaultValueX(fType);
+        Parameter param = new Parameter(type, name);
+        switch (defaultsMode) {
+          case ON:
+              if (init == null || (ClassHelper.isPrimitiveType(fType) && ExpressionUtils.isNullConstant(init)))
+                  init = defaultValueX(fType);
+              // falls through
+          case AUTO:
+              if (init != null) {
+                  param.setInitialExpression(init);
+                  fNode.setInitialValueExpression(null); // GROOVY-10238
+              }
+            break;
+          default:
+            if (init != null && !makeImmutable) {
+                xform.addError("Error during " + MY_TYPE_NAME + " processing, default value processing disabled but default value found for '" + fNode.getName() + "'", fNode);
+            }
         }
-        return init;
+        return param;
     }
 
     public static void addSpecialMapConstructors(final int modifiers, final ClassNode cNode, final String message, final boolean addNoArg) {
@@ -350,8 +347,8 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         VariableExpression namedArgs = varX(NAMED_ARGS);
         namedArgs.setAccessedVariable(parameters[0]);
         code.addStatement(ifElseS(equalsNullX(namedArgs),
-                illegalArgumentBlock(message),
-                processArgsBlock(cNode, namedArgs)));
+                throwS(ctorX(ClassHelper.make(IllegalArgumentException.class), args(constX(message)))),
+                processNamedArgs(cNode, namedArgs)));
         addGeneratedConstructor(cNode, modifiers, parameters, ClassNode.EMPTY_ARRAY, code);
         // potentially add a no-arg constructor too
         if (addNoArg) {
@@ -361,17 +358,13 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         }
     }
 
-    private static BlockStatement illegalArgumentBlock(final String message) {
-        return block(throwS(ctorX(ClassHelper.make(IllegalArgumentException.class), args(constX(message)))));
-    }
-
-    private static BlockStatement processArgsBlock(final ClassNode cNode, final VariableExpression namedArgs) {
+    private static BlockStatement processNamedArgs(final ClassNode cNode, final VariableExpression namedArgs) {
         BlockStatement block = new BlockStatement();
         List<PropertyNode> props = new ArrayList<>();
         for (PropertyNode pNode : cNode.getProperties()) {
             if (pNode.isStatic()) continue;
 
-            // if (namedArgs.containsKey(propertyName)) propertyNode= namedArgs.propertyName;
+            // if (namedArgs.containsKey(propertyName)) propertyNode = namedArgs.propertyName;
             MethodCallExpression containsProperty = callX(namedArgs, "containsKey", constX(pNode.getName()));
             containsProperty.setImplicitThis(false);
             block.addStatement(ifS(containsProperty, assignS(varX(pNode), propX(namedArgs, pNode.getName()))));
@@ -382,7 +375,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         return block;
     }
 
-    private static DefaultsMode maybeDefaultsMode(AnnotationNode node, String name) {
+    private static DefaultsMode maybeDefaultsMode(final AnnotationNode node, final String name) {
         if (node != null) {
             final Expression member = node.getMember(name);
             if (member instanceof ConstantExpression) {
@@ -403,9 +396,4 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         }
         return null;
     }
-
-    @Override
-    public int priority() {
-        return 5;
-    }
 }
diff --git a/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy b/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
index e74dac7e94..cab5f1d309 100644
--- a/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
+++ b/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
@@ -18,62 +18,40 @@
  */
 package org.codehaus.groovy.transform
 
-import groovy.test.GroovyShellTestCase
 import org.codehaus.groovy.control.MultipleCompilationErrorsException
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestName
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
-import static groovy.test.GroovyAssert.isAtLeastJdk
-import static org.junit.Assume.assumeTrue
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
 
 /**
- * Tests for the @Immutable transform.
+ * Tests for the {@code @Immutable} transform.
  */
-@RunWith(JUnit4)
-class ImmutableTransformTest extends GroovyShellTestCase {
+final class ImmutableTransformTest {
 
-    @Rule public TestName nameRule = new TestName()
-
-    @Before
-    void setUp() {
-        super.setUp()
-        // check java version requirements
-        assumeTrue(nameRule.methodName.endsWith('_vm8').implies(isAtLeastJdk('1.8')))
-    }
-
-    @After
-    void tearDown() {
-        super.tearDown()
+    private final GroovyShell shell = GroovyShell.withConfig {
+        imports { star 'groovy.transform' }
     }
 
     @Test
     void testImmutable() {
-        def objects = evaluate('''
-            import groovy.transform.Immutable
+        def objects = shell.evaluate '''
             enum Coin { HEAD, TAIL }
             @Immutable class Bar {
                 String x, y
                 Coin c
                 Collection nums
             }
-            [new Bar(x:'x', y:'y', c:Coin.HEAD, nums:[1,2]),
-             new Bar('x', 'y', Coin.HEAD, [1,2])]
-        ''')
-
+            [new Bar(x:'x', y:'y', c:Coin.HEAD, nums:[1,2]), new Bar('x', 'y', Coin.HEAD, [1,2])]
+        '''
         assert objects[0].hashCode() == objects[1].hashCode()
         assert objects[0] == objects[1]
-        assert objects[0].nums.class.name.contains("Unmodifiable")
+        assert objects[0].nums.class.name.contains('Unmodifiable')
     }
 
     @Test
     void testImmutableClonesListAndCollectionFields() {
-        def objects = evaluate("""
-            import groovy.transform.Immutable
+        def objects = shell.evaluate '''
             def myNums = [1, 2]
             @Immutable class Bar {
                 List nums
@@ -82,27 +60,25 @@ class ImmutableTransformTest extends GroovyShellTestCase {
             def myBar = new Bar(nums:myNums, otherNums:myNums)
             myNums << 3
             [myNums, myBar]
-        """)
-
-        assertNotSame(objects[0], objects[1].nums)
-        assertNotSame(objects[0], objects[1].otherNums)
-        assertNotSame(objects[1].nums, objects[1].otherNums)
-        assertEquals 3, objects[0].size()
-        assertEquals 2, objects[1].nums.size()
-        assertEquals 2, objects[1].otherNums.size()
-        assertTrue objects[1].nums.class.name.contains("Unmodifiable")
-        assertTrue objects[1].otherNums.class.name.contains("Unmodifiable")
+        '''
+        assert objects[0] !== objects[1].nums
+        assert objects[0] !== objects[1].otherNums
+        assert objects[1].nums !== objects[1].otherNums
+        assert objects[0].size() == 3
+        assert objects[1].nums.size() == 2
+        assert objects[1].otherNums.size() == 2
+        assert objects[1].nums.class.name.contains('Unmodifiable')
+        assert objects[1].otherNums.class.name.contains('Unmodifiable')
     }
 
     @Test
     void testImmutableField() {
-        def person = evaluate("""
-            import groovy.transform.Immutable
+        def person = shell.evaluate '''
             @Immutable class Person {
                 boolean married
             }
             new Person(married:false)
-        """)
+        '''
         shouldFail(ReadOnlyPropertyException) {
             person.married = true
         }
@@ -110,9 +86,7 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testCloneableField() {
-        def (originalDolly, lab) = evaluate('''
-            import groovy.transform.*
-
+        def (originalDolly, lab) = shell.evaluate '''
             @AutoClone
             class Dolly implements Cloneable {
                 String name
@@ -125,11 +99,9 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
             def dolly = new Dolly(name: "The Sheep")
             [dolly, new Lab(name: "Area 51", clone: dolly)]
-        ''')
-
+        '''
         def clonedDolly = lab.clone
         def clonedDolly2 = lab.clone
-
         assert lab.name == 'Area 51'
         assert !originalDolly.is(clonedDolly)
         assert originalDolly.name == clonedDolly.name
@@ -139,47 +111,41 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testCloneableFieldNotCloneableObject() {
-        shouldFail(CloneNotSupportedException, '''
-                import groovy.transform.Immutable
-
-                class Dolly {
-                    String name
-                }
+        shouldFail shell, CloneNotSupportedException, '''
+            class Dolly {
+                String name
+            }
 
-                @Immutable class Lab {
-                    String name
-                    Cloneable clone
-                }
+            @Immutable class Lab {
+                String name
+                Cloneable clone
+            }
 
-                def dolly = new Dolly(name: "The Sheep")
-                [dolly, new Lab(name: "Area 51", clone: dolly)]
-        ''')
+            def dolly = new Dolly(name: "The Sheep")
+            [dolly, new Lab(name: "Area 51", clone: dolly)]
+        '''
     }
 
     @Test
     void testImmutableListProp() {
-        def objects = evaluate("""
-            import groovy.transform.Immutable
+        def objects = shell.evaluate '''
             @Immutable class HasList {
                 String[] letters
                 List nums
             }
             def letters = 'A,B,C'.split(',')
             def nums = [1, 2]
-            [new HasList(letters:letters, nums:nums),
-             new HasList(letters, nums)]
-        """)
-
-        assertEquals objects[0].hashCode(), objects[1].hashCode()
-        assertEquals objects[0], objects[1]
+            [new HasList(letters:letters, nums:nums), new HasList(letters, nums)]
+        '''
+        assert objects[0].hashCode() == objects[1].hashCode()
+        assert objects[0] == objects[1]
         assert objects[0].letters.size() == 3
         assert objects[0].nums.size() == 2
     }
 
     @Test
     void testImmutableAsMapKey() {
-        assertScript """
-            import groovy.transform.Immutable
+        assertScript shell, '''
             @Immutable final class HasString {
                 String s
             }
@@ -187,49 +153,43 @@ class ImmutableTransformTest extends GroovyShellTestCase {
             def k2 = new HasString('xyz')
             def map = [(k1):42]
             assert map[k2] == 42
-        """
+        '''
     }
 
     @Test
     void testImmutableWithOnlyMap() {
-        assertScript """
-            import groovy.transform.Immutable
+        assertScript shell, '''
             @Immutable final class HasMap {
                 Map map
             }
             new HasMap([:])
-        """
+        '''
     }
 
     @Test
     void testImmutableWithPrivateStaticFinalField() {
-        assertScript """
-          @groovy.transform.Immutable class Foo {
-              private static final String BAR = 'baz'
-          }
-          assert new Foo().BAR == 'baz'
-      """
+        assertScript shell, '''
+            @Immutable class Foo {
+                private static final String BAR = 'baz'
+            }
+            assert new Foo().BAR == 'baz'
+        '''
     }
 
     @Test
     void testImmutableWithInvalidPropertyName() {
-        def msg = shouldFail(MissingPropertyException) {
-            assertScript """
-                import groovy.transform.Immutable
-                @Immutable class Simple { }
-                new Simple(missing:'Name')
-            """
-        }
-        assert msg.contains('No such property: missing for class: Simple')
+        def err = shouldFail shell, MissingPropertyException, '''
+            @Immutable class Simple {}
+            new Simple(missing:'Name')
+        '''
+        assert err =~ 'No such property: missing for class: Simple'
     }
 
     @Test
     void testImmutableWithHashMap() {
-        assertScript """
-            import groovy.transform.Immutable
-            import groovy.transform.options.LegacyHashMapPropertyHandler
-            @Immutable(propertyHandler = LegacyHashMapPropertyHandler, noArg = false)
-            final class HasHashMap {
+        assertScript shell, '''
+            @Immutable(propertyHandler=groovy.transform.options.LegacyHashMapPropertyHandler, noArg=false)
+            class HasHashMap {
                 HashMap map = [d:4]
             }
             assert new HasHashMap([a:1]).map == [a:1]
@@ -241,37 +201,12 @@ class ImmutableTransformTest extends GroovyShellTestCase {
             assert new HasHashMap(map:5, c:3).map == [map:5, c:3]
             assert new HasHashMap(map:null).map == null
             assert new HasHashMap(map:[:]).map == [:]
-        """
-    }
-
-    @Test
-    void testDefaultValuesAreImmutable_groovy6293() {
-        assertScript """
-            import groovy.transform.Immutable
-            @Immutable class Y { Collection c = []; int foo = 1 }
-            def y = new Y(foo: 3)
-            assert y.c.class.name.contains('Unmodifiable')
-            assert y.c == []
-            assert y.foo == 3
-        """
-    }
-
-    @Test
-    void testNoArgConstructor_groovy6473() {
-        assertScript """
-            import groovy.transform.Immutable
-            @Immutable class Y { Collection c = []; int foo = 1 }
-            def y = new Y()
-            assert y.c.class.name.contains('Unmodifiable')
-            assert y.c == []
-            assert y.foo == 1
-        """
+        '''
     }
 
     @Test
     void testImmutableEquals() {
-        assertScript """
-            import groovy.transform.Immutable
+        assertScript shell, """
             @Immutable class This { String value }
             @Immutable class That { String value }
             class Other { }
@@ -288,8 +223,7 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testExistingToString() {
-        assertScript """
-            import groovy.transform.Immutable
+        assertScript shell, '''
             @Immutable class Foo {
                 String value
             }
@@ -307,13 +241,12 @@ class ImmutableTransformTest extends GroovyShellTestCase {
             def baz = new Baz('abc')
             assert bar.toString() == 'zzz' + foo.toString().replaceAll('Foo', 'Bar')
             assert baz.toString() == 'zzzxxx'
-        """
+        '''
     }
 
     @Test
     void testExistingEquals() {
-        assertScript """
-            import groovy.transform.Immutable
+        assertScript shell, '''
             @Immutable class Foo {
                 String value
             }
@@ -350,13 +283,12 @@ class ImmutableTransformTest extends GroovyShellTestCase {
             assert baz1 == baz3
             assert baz3 != baz1
             assert baz3 != baz4
-        """
+        '''
     }
 
     @Test
     void testExistingHashCode() {
-        assertScript """
-            import groovy.transform.Immutable
+        assertScript shell, '''
             @Immutable class Foo {
                 String value
             }
@@ -385,14 +317,13 @@ class ImmutableTransformTest extends GroovyShellTestCase {
             def baz2 = new Baz('def')
             assert baz1.hashCode() == -1
             assert baz2.hashCode() == -100
-        """
+        '''
     }
 
     @Test
     void testBuiltinImmutables() {
-        assertScript '''
+        assertScript shell, '''
             import java.awt.Color
-            import groovy.transform.Immutable
 
             @Immutable class Person {
                 UUID id
@@ -410,8 +341,7 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testPrivateFieldAssignedViaConstructor() {
-        assertScript '''
-            import groovy.transform.Immutable
+        assertScript shell, '''
             @Immutable(includeStatic = true)
             class Numbers {
                 private int a1 = 1
@@ -428,28 +358,23 @@ class ImmutableTransformTest extends GroovyShellTestCase {
                 private static final int c4 = 4
             }
             def n1 = new Numbers(b1:1, b3:3, c1:1, c2:2, c3:3)
-            assert [1..4, 'a'..'c'].combinations().collect{ num, let -> n1."$let$num" } ==
-                       [1, 2, 3, 4, 1, -2, 3, -4, 1, 2, 3, 4]
+            assert [1..4, 'a'..'c'].combinations().collect{ num, let -> n1."$let$num" } == [1, 2, 3, 4, 1, -2, 3, -4, 1, 2, 3, 4]
         '''
     }
 
     @Test
     void testPrivateFinalFieldAssignedViaConstructorShouldCauseError() {
-        shouldFail(ReadOnlyPropertyException) {
-            evaluate '''
-                import groovy.transform.Immutable
-                @Immutable class Numbers {
-                    private final int b2 = -2
-                }
-                def n1 = new Numbers(b2:2)
-            '''
-        }
+        shouldFail shell, ReadOnlyPropertyException, '''
+            @Immutable class Numbers {
+                private final int b2 = -2
+            }
+            def n1 = new Numbers(b2:2)
+        '''
     }
 
     @Test
     void testImmutableWithImmutableFields() {
-        assertScript '''
-            import groovy.transform.Immutable
+        assertScript shell, '''
             @Immutable class Bar { Integer i }
             @Immutable class Foo { Bar b }
             def fb = new Foo(new Bar(3))
@@ -459,8 +384,7 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testImmutableWithConstant() {
-        assertScript '''
-            import groovy.transform.Immutable
+        assertScript shell, '''
             @Immutable class MinIntegerHolder {
                 Integer i
                 public static final MIN = 3
@@ -476,8 +400,7 @@ class ImmutableTransformTest extends GroovyShellTestCase {
     @Test
     void testStaticsAllowed_ThoughUsuallyBadDesign() {
         // design here is questionable as getDescription() method is not idempotent
-        assertScript '''
-            import groovy.transform.Immutable
+        assertScript shell, '''
             @Immutable class Person {
                String first, last
                static species = 'Human'
@@ -506,9 +429,7 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testImmutableToStringVariants() {
-        assertScript '''
-            import groovy.transform.*
-
+        assertScript shell, '''
             @Immutable
             class Person1 { String first, last }
 
@@ -526,10 +447,9 @@ class ImmutableTransformTest extends GroovyShellTestCase {
         '''
     }
 
-    @Test
+    @Test // GROOVY-4997
     void testImmutableUsageOnInnerClasses() {
-        assertScript '''
-            import groovy.transform.Immutable
+        assertScript shell, '''
             class A4997 {
                 @Immutable
                 static class B4997 { String name }
@@ -546,8 +466,7 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testKnownImmutableClassesWithNamedParameters() {
-        assertScript '''
-            import groovy.transform.*
+        assertScript shell, '''
             @Immutable(knownImmutableClasses = [Address])
             class Person {
                 String first, last
@@ -562,8 +481,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testKnownImmutableClassesWithExplicitConstructor() {
-        assertScript '''
-            @groovy.transform.Immutable(knownImmutableClasses = [Address])
+        assertScript shell, '''
+            @Immutable(knownImmutableClasses = [Address])
             class Person {
                 String first, last
                 Address address
@@ -578,8 +497,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testKnownImmutableClassesWithCoercedConstruction() {
-        assertScript '''
-            @groovy.transform.Immutable(knownImmutableClasses = [Address])
+        assertScript shell, '''
+            @Immutable(knownImmutableClasses = [Address])
             class Person {
                 String first, last
                 Address address
@@ -594,34 +513,32 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testKnownImmutableClassesMissing() {
-        def msg = shouldFail(RuntimeException) {
-            evaluate '''
-                @groovy.transform.ToString class Address { String street }
-
-                @groovy.transform.Immutable
-                class Person {
-                    String first, last
-                    Address address
-                }
+        def err = shouldFail shell, RuntimeException, '''
+            @ToString class Address { String street }
 
-                new Person(first: 'John', last: 'Doe', address: new Address(street: 'Street'))
-            '''
-        }
-        assert msg.contains("Unsupported type (Address) found for field 'address' while constructing immutable class Person")
-        assert msg.contains("Immutable classes only support properties with effectively immutable types")
+            @Immutable
+            class Person {
+                String first, last
+                Address address
+            }
+
+            new Person(first: 'John', last: 'Doe', address: new Address(street: 'Street'))
+        '''
+        assert err.message.contains("Unsupported type (Address) found for field 'address' while constructing immutable class Person")
+        assert err.message.contains("Immutable classes only support properties with effectively immutable types")
     }
 
     // GROOVY-5828
     @Test
     void testKnownImmutableCollectionClass() {
-        assertScript '''
-            @groovy.transform.Immutable
+        assertScript shell, '''
+            @Immutable
             class ItemsControl { List list }
             def itemsControl = new ItemsControl(['Fee', 'Fi', 'Fo', 'Fum'])
             assert itemsControl.list.class.name.contains('Unmodifiable')
 
             // ok, Items not really immutable but pretend so for the purpose of this test
-            @groovy.transform.Immutable(knownImmutableClasses = [List])
+            @Immutable(knownImmutableClasses = [List])
             class Items { List list }
             def items = new Items(['Fee', 'Fi', 'Fo', 'Fum'])
             assert !items.list.class.name.contains('Unmodifiable')
@@ -631,9 +548,9 @@ class ImmutableTransformTest extends GroovyShellTestCase {
     // GROOVY-5828
     @Test
     void testKnownImmutables() {
-        assertScript '''
+        assertScript shell, '''
             // ok, Items not really immutable but pretend so for the purpose of this test
-            @groovy.transform.Immutable(knownImmutables = ['list1'])
+            @Immutable(knownImmutables = ['list1'])
             class Items {
                 List list1
                 List list2
@@ -647,23 +564,19 @@ class ImmutableTransformTest extends GroovyShellTestCase {
     // GROOVY-5449
     @Test
     void testShouldNotThrowNPE() {
-        def msg = shouldFail(RuntimeException) {
-            evaluate '''
-            @groovy.transform.Immutable
+        def err = shouldFail shell, RuntimeException, '''
+            @Immutable
             class Person {
                 def name
             }
-            '''
-        }
-        assert msg.contains("Unsupported type (java.lang.Object or def) found for field 'name' while ")
+        '''
+        assert err.message.contains("Unsupported type (java.lang.Object or def) found for field 'name' while ")
     }
 
     // GROOVY-6192
     @Test
     void testWithEqualsAndHashCodeASTOverride() {
-        assertScript '''
-            import groovy.transform.*
-
+        assertScript shell, '''
             @Immutable
             @EqualsAndHashCode(includes = ['id'])
             class B {
@@ -678,8 +591,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
     // GROOVY-6354
     @Test
     void testCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
+        def tester = new GroovyClassLoader().parseClass('''\
+            |@groovy.transform.Immutable(copyWith = true)
             |class Person {
             |    String first, last
             |}
@@ -714,8 +627,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testGenericsCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
+        def tester = new GroovyClassLoader().parseClass('''\
+            |@groovy.transform.Immutable(copyWith = true)
             |class Person {
             |    List<String> names
             |}
@@ -738,8 +651,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testWithPrivatesCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith=true)
+        def tester = new GroovyClassLoader().parseClass('''\
+            |@groovy.transform.Immutable(copyWith=true)
             |class Foo {
             |  String first
             |  String last
@@ -769,8 +682,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testStaticWithPrivatesCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith=true)
+        def tester = new GroovyClassLoader().parseClass('''\
+            |@groovy.transform.Immutable(copyWith=true)
             |@groovy.transform.CompileStatic
             |class Foo {
             |  String first
@@ -801,8 +714,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testTypedWithPrivatesCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith=true)
+        def tester = new GroovyClassLoader().parseClass('''\
+            |@groovy.transform.Immutable(copyWith=true)
             |@groovy.transform.TypeChecked
             |class Foo {
             |  String first
@@ -833,8 +746,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testStaticCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
+        def tester = new GroovyClassLoader().parseClass('''\
+            |@groovy.transform.Immutable(copyWith = true)
             |@groovy.transform.CompileStatic
             |class Person {
             |    String first, last
@@ -870,8 +783,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testTypedCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
+        def tester = new GroovyClassLoader().parseClass('''\
+            |@groovy.transform.Immutable(copyWith = true)
             |@groovy.transform.TypeChecked
             |class Person {
             |    String first, last
@@ -907,8 +820,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testCopyWithSkipping() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
+        def tester = new GroovyClassLoader().parseClass('''\
+            |@groovy.transform.Immutable(copyWith = true)
             |class Person {
             |    String first, last
             |    List<Person> copyWith( i ) {
@@ -929,8 +842,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testStaticCopyWithSkipping() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
+        def tester = new GroovyClassLoader().parseClass('''\
+            |@groovy.transform.Immutable(copyWith = true)
             |@groovy.transform.CompileStatic
             |class Person {
             |    String first, last
@@ -952,8 +865,8 @@ class ImmutableTransformTest extends GroovyShellTestCase {
 
     @Test
     void testTypedCopyWithSkipping() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
+        def tester = new GroovyClassLoader().parseClass('''\
+            |@groovy.transform.Immutable(copyWith = true)
             |@groovy.transform.TypeChecked
             |class Person {
             |    String first, last
@@ -973,28 +886,47 @@ class ImmutableTransformTest extends GroovyShellTestCase {
         assert result.first == [ 'tim', 'tim' ]
     }
 
+    // GROOVY-6293
+    @Test
+    void testDefaultValuesAreImmutable() {
+        assertScript shell, '''
+            @Immutable class Y { Collection c = []; int foo = 1 }
+            def y = new Y(foo: 3)
+            assert y.c.class.name.contains('Unmodifiable')
+            assert y.c == []
+            assert y.foo == 3
+        '''
+    }
+
+    // GROOVY-6473
+    @Test
+    void testNoArgConstructor() {
+        assertScript shell, '''
+            @Immutable class Y { Collection c = []; int foo = 1 }
+            def y = new Y()
+            assert y.c.class.name.contains('Unmodifiable')
+            assert y.c == []
+            assert y.foo == 1
+        '''
+    }
+
     // GROOVY-7227
     @Test
     void testKnownImmutablesWithInvalidPropertyNameResultsInError() {
-        def message = shouldFail {
-            evaluate """
-               import groovy.transform.Immutable
-               @Immutable(knownImmutables=['sirName'])
-               class Person {
-                   String surName
-               }
-               new Person(surName: "Doe")
-           """
-        }
-        assert message.contains("Error during immutable class processing: 'knownImmutables' property 'sirName' does not exist.")
+        def err = shouldFail shell, '''
+            @Immutable(knownImmutables=['sirName'])
+            class Person {
+                String surName
+            }
+            new Person(surName: "Doe")
+        '''
+        assert err.message.contains("Error during immutable class processing: 'knownImmutables' property 'sirName' does not exist.")
     }
 
     // GROOVY-7162
     @Test
     void testImmutableWithSuperClass() {
-        assertScript '''
-            import groovy.transform.*
-
+        assertScript shell, '''
             @EqualsAndHashCode
             class Person {
                 String name
@@ -1016,33 +948,10 @@ class ImmutableTransformTest extends GroovyShellTestCase {
         '''
     }
 
-    // GROOVY-7600
-    @Test
-    void testImmutableWithOptional_vm8() {
-        assertScript '''
-            @groovy.transform.Immutable class Person {
-                String name
-                Optional<String> address
-            }
-            def p = new Person('Joe', Optional.of('Home'))
-            assert p.toString() == 'Person(Joe, Optional[Home])'
-            assert p.address.get() == 'Home'
-        '''
-        shouldFail(MultipleCompilationErrorsException) {
-            evaluate '''
-            @groovy.transform.Immutable class Person {
-                String name
-                Optional<Date> address
-            }
-            '''
-        }
-    }
-
     // GROOVY-7599
     @Test
-    void testImmutableWithJSR310_vm8() {
-        assertScript '''
-            import groovy.transform.Immutable
+    void testImmutableWithJSR310() {
+        assertScript shell, '''
             import java.time.*
 
             @Immutable
@@ -1056,11 +965,30 @@ class ImmutableTransformTest extends GroovyShellTestCase {
         '''
     }
 
+    // GROOVY-7600
+    @Test
+    void testImmutableWithOptional() {
+        assertScript shell, '''
+            @Immutable class Person {
+                String name
+                Optional<String> address
+            }
+            def p = new Person('Joe', Optional.of('Home'))
+            assert p.toString() == 'Person(Joe, Optional[Home])'
+            assert p.address.get() == 'Home'
+        '''
+        shouldFail shell, MultipleCompilationErrorsException, '''
+            @Immutable class Person {
+                String name
+                Optional<Date> address
+            }
+        '''
+    }
+
     // GROOVY-8416
     @Test
     void testMapFriendlyNamedArgs() {
-        assertScript '''
-            import groovy.transform.Immutable
+        assertScript shell, '''
             @Immutable
             class Point {
                 int x, y
@@ -1079,9 +1007,7 @@ class ImmutableTransformTest extends GroovyShellTestCase {
     // GROOVY-8967
     @Test
     void testPropertiesWithDefaultValues() {
-        assertScript '''
-            import groovy.transform.*
-
+        assertScript shell, '''
             @Immutable
             class Thing {
                 int i = 42
@@ -1096,4 +1022,27 @@ class ImmutableTransformTest extends GroovyShellTestCase {
             assert thing.with{ [i, c, d, value] } == [-1, null, null, null]
         '''
     }
+
+    // GROOVY-10790
+    @Test
+    void testDefaultsTrueExtraConstructors() {
+        assertScript shell, '''
+            @Immutable(defaults=true, noArg=false)
+            class Foo {
+                String bar, baz = 'z'
+            }
+            assert new Foo('x','y').toString() == 'Foo(x, y)'
+            assert new Foo('x').toString() == 'Foo(x, z)'
+            assert new Foo().toString() == 'Foo(null, z)'
+        '''
+        assertScript shell, '''
+            @Immutable(defaults=true) // MapConstructor also creates no-arg ctor
+            class Foo {
+                String bar, baz = 'z'
+            }
+            assert new Foo('x','y').toString() == 'Foo(x, y)'
+            assert new Foo('x').toString() == 'Foo(x, z)'
+            assert new Foo().toString() == 'Foo(null, z)'
+        '''
+    }
 }
diff --git a/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy b/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
index 9976e5be22..228690b108 100644
--- a/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
+++ b/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
@@ -18,27 +18,38 @@
  */
 package org.codehaus.groovy.transform
 
-import groovy.test.GroovyShellTestCase
+import org.junit.Test
 
-class TupleConstructorTransformTest extends GroovyShellTestCase {
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
 
+/**
+ * Tests for the {@code TupleConstructor} transform.
+ */
+final class TupleConstructorTransformTest {
+
+    private final GroovyShell shell = GroovyShell.withConfig {
+        imports { star 'groovy.transform' }
+    }
+
+    @Test
     void testBasics() {
-        assertScript '''
-            @groovy.transform.TupleConstructor
+        assertScript shell, '''
+            @TupleConstructor(defaults=false)
             class Person {
-                String firstName
-                String lastName
+                String firstName, lastName
             }
 
             def p = new Person('John', 'Doe')
             assert p.firstName == 'John'
-            assert p.lastName == 'Doe'
+            assert p.lastName  == 'Doe'
         '''
     }
 
+    @Test
     void testCopyConstructor() {
-        assertScript '''
-            @groovy.transform.TupleConstructor(force=true)
+        assertScript shell, '''
+            @TupleConstructor(force=true)
             class Person {
                 String firstName, lastName
                 Person(Person that) {
@@ -48,17 +59,18 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
 
             def p = new Person('John', 'Doe')
             assert p.firstName == 'John'
-            assert p.lastName == 'Doe'
+            assert p.lastName  == 'Doe'
 
             p = new Person(p)
             assert p.firstName == 'John'
-            assert p.lastName == null
+            assert p.lastName  == null
         '''
     }
 
+    @Test
     void testFieldsAndInitializers() {
-        assertScript '''
-            @groovy.transform.TupleConstructor(includeFields=true)
+        assertScript shell, '''
+            @TupleConstructor(includeFields=true)
             class Person {
                 String firstName = 'John'
                 private String lastName = 'Doe'
@@ -67,18 +79,21 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
 
             def p = new Person()
             assert p.firstName == 'John'
-            assert p.lastName == 'Doe'
+            assert p.lastName  == 'Doe'
 
             p = new Person('Jane')
             assert p.firstName == 'Jane'
-            assert p.lastName == 'Doe'
+            assert p.lastName  == 'Doe'
+
+            p = new Person('Jane', 'Eyre')
+            assert p.firstName == 'Jane'
+            assert p.lastName  == 'Eyre'
         '''
     }
 
+    @Test
     void testFieldsAndNamesAndPost() {
-        assertScript '''
-            import groovy.transform.*
-
+        assertScript shell, '''
             @ToString(includeFields=true, includeNames=true)
             @TupleConstructor(post={ full = "$first $last" })
             class Person {
@@ -86,15 +101,13 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
                 private final String full
             }
 
-            assert new Person('Dierk', 'Koenig').toString() ==
-                'Person(first:Dierk, last:Koenig, full:Dierk Koenig)'
+            assert new Person('Dierk', 'Koenig').toString() == 'Person(first:Dierk, last:Koenig, full:Dierk Koenig)'
         '''
     }
 
+    @Test
     void testSuperPropsAndPreAndPost() {
-        assertScript '''
-            import groovy.transform.*
-
+        assertScript shell, '''
             @TupleConstructor
             class Person {
                 String first, last
@@ -102,7 +115,7 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
 
             @CompileStatic // optional
             @ToString(includeSuperProperties=true)
-            @TupleConstructor(includeSuperProperties=true, pre={ super(first, last?.toLowerCase()) }, post = { this.first = this.first?.toUpperCase() })
+            @TupleConstructor(includeSuperProperties=true, pre={ super(first, last?.toLowerCase()) }, post={ this.first = this.first?.toUpperCase() })
             class Author extends Person {
                 String bookName
             }
@@ -113,9 +126,10 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
     }
 
     // GROOVY-7522
-    void testExistingEmptyConstructorTakesPrecedence() {
-        assertScript '''
-            @groovy.transform.TupleConstructor
+    @Test
+    void testExistingConstructorTakesPrecedence() {
+        assertScript shell, '''
+            @TupleConstructor
             class Cat {
                 String name
                 int age
@@ -125,75 +139,74 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
             assert new Cat("Mr. Bigglesworth").name == null
             assert Cat.declaredConstructors.size() == 1
         '''
+        assertScript shell, '''
+            @TupleConstructor(force=true)
+            class Cat {
+                String name
+                int age
+                Cat(String name) {}
+            }
+
+            assert new Cat().name == null
+            assert new Cat("Mr. Bigglesworth").name == null
+            assert new Cat("Mr. Bigglesworth", 42).name == "Mr. Bigglesworth"
+            assert Cat.declaredConstructors.size() == 3 // (), (String) and (String,int)
+        '''
     }
 
+    @Test
     void testIncludesAndExcludesTogetherResultsInError() {
-        def message = shouldFail {
-            evaluate '''
-                import groovy.transform.TupleConstructor
-
-                @TupleConstructor(includes='surName', excludes='surName')
-                class Person {
-                    String surName
-                }
-
-                new Person("Doe")
-            '''
-        }
-        assert message.contains("Error during @TupleConstructor processing: Only one of 'includes' and 'excludes' should be supplied not both.")
+        def err = shouldFail shell, '''
+            @TupleConstructor(includes='surName', excludes='surName')
+            class Person {
+                String surName
+            }
+        '''
+        assert err.message.contains("Error during @TupleConstructor processing: Only one of 'includes' and 'excludes' should be supplied not both.")
     }
 
+    @Test
     void testIncludesWithInvalidPropertyNameResultsInError() {
-        def message = shouldFail {
-            evaluate '''
-                import groovy.transform.TupleConstructor
-
-                @TupleConstructor(includes='sirName')
-                class Person {
-                    String firstName
-                    String surName
-                }
-
-                def p = new Person("John", "Doe")
-            '''
-        }
-        assert message.contains("Error during @TupleConstructor processing: 'includes' property 'sirName' does not exist.")
+        def err = shouldFail shell, '''
+            @TupleConstructor(includes='sirName')
+            class Person {
+                String firstName
+                String surName
+            }
+        '''
+        assert err.message.contains("Error during @TupleConstructor processing: 'includes' property 'sirName' does not exist.")
     }
 
+    @Test
     void testExcludesWithInvalidPropertyNameResultsInError() {
-        def message = shouldFail {
-            evaluate '''
-                import groovy.transform.TupleConstructor
-
-                @TupleConstructor(excludes='sirName')
-                class Person {
-                    String firstName
-                    String surName
-                }
-
-                def p = new Person("John", "Doe")
-            '''
-        }
-        assert message.contains("Error during @TupleConstructor processing: 'excludes' property 'sirName' does not exist.")
+        def err = shouldFail shell, '''
+            @TupleConstructor(excludes='sirName')
+            class Person {
+                String firstName
+                String surName
+            }
+        '''
+        assert err.message.contains("Error during @TupleConstructor processing: 'excludes' property 'sirName' does not exist.")
     }
 
     // GROOVY-7523
+    @Test
     void testIncludesWithEmptyList() {
-        assertScript '''
-            @groovy.transform.TupleConstructor(includes=[])
+        assertScript shell, '''
+            @TupleConstructor(includes=[])
             class Cat {
                 String name
                 int age
             }
+
             assert Cat.declaredConstructors.size() == 1
         '''
     }
 
     // GROOVY-7524
+    @Test
     void testCombiningWithInheritConstructors() {
-        assertScript '''
-            import groovy.transform.*
-
+        assertScript shell, '''
             @TupleConstructor
             class NameId {
                 String name
@@ -217,9 +230,9 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
     }
 
     // GROOVY-7672
+    @Test
     void testMultipleUsages() {
-        assertScript '''
-            import groovy.transform.*
+        assertScript shell, '''
             import java.awt.Color
 
             class Named {
@@ -245,11 +258,10 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
     }
 
     // GROOVY-6454
+    @Test
     void testInternalFieldsAreIncludedIfRequested() {
-        assertScript '''
-            import groovy.transform.*
-
-            @TupleConstructor(allNames = true)
+        assertScript shell, '''
+            @TupleConstructor(allNames=true)
             class HasInternalName {
                 String $internal
             }
@@ -259,18 +271,17 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
     }
 
     // GROOVY-7981
+    @Test
     void testVisibilityOptions() {
-        assertScript '''
-            import groovy.transform.*
+        assertScript shell, '''
             import static groovy.transform.options.Visibility.*
             import static java.lang.reflect.Modifier.isPrivate
 
             @VisibilityOptions(PRIVATE)
             @Immutable
-            @ASTTest(phase = CANONICALIZATION,
-                     value = {
-                         node.constructors.every { isPrivate(it.modifiers) }
-                     })
+            @ASTTest(phase=CANONICALIZATION, value={
+                node.constructors.every { isPrivate(it.modifiers) }
+            })
             class Person {
                 String first, last
                 int age
@@ -280,33 +291,32 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
             }
 
             @CompileStatic
-            def method() {
+            void test() {
                 def p = Person.makePerson(first: 'John', last: 'Smith', age: 20)
                 assert p.toString() == 'Person(John, Smith, 20)'
             }
-            method()
+            test()
         '''
     }
 
     // GROOVY-7981
+    @Test
     void testMultipleVisibilityOptions() {
-        assertScript '''
-            import groovy.transform.*
-            import java.lang.reflect.Modifier
+        assertScript shell, '''
             import static groovy.transform.options.Visibility.*
-
-            @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)
-                         }
-                     })
+            import static java.lang.reflect.Modifier.*
+
+            @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)' && isProtected(it.modifiers)) ||
+                            (it.typeDescriptor == 'void <init>(int)' && isPrivate(it.modifiers))
+                }
+            })
             class Person {
                 String first, last
                 int age
@@ -315,15 +325,15 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
                     assert new Person(42).age == 42
                 }
             }
+
             Person.test()
         '''
     }
 
     // GROOVY-8455, GROOVY-8453
+    @Test
     void testPropPsuedoPropAndFieldOrderIncludingInheritedMembers() {
-        assertScript '''
-            import groovy.transform.TupleConstructor
-
+        assertScript shell, '''
             class Basepubf{}
             class Basep{}
             class Basepp{}
@@ -371,27 +381,26 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
     }
 
     // GROOVY-10361
-    void testTupleConstructorDefaultsModes() {
-        assertScript '''
-            import groovy.transform.*
-            import static groovy.test.GroovyAssert.shouldFail
-
-            @TupleConstructor(defaultsMode = DefaultsMode.OFF, includeFields = true)
+    @Test
+    void testDefaultsMode() {
+        assertScript shell, '''
+            @TupleConstructor(defaultsMode=DefaultsMode.OFF, includeFields=true)
             class A {
                 String won
                 private int too
             }
-            assert A.declaredConstructors.toString() == '[public A(java.lang.String,int)]'
 
-            shouldFail """
-            @TupleConstructor(defaultsMode = DefaultsMode.OFF, includeFields = true)
+            assert A.declaredConstructors.toString() == '[public A(java.lang.String,int)]'
+        '''
+        shouldFail shell, '''
+            @TupleConstructor(defaultsMode=DefaultsMode.OFF, includeFields=true)
             class B {
                 String won = 'one'
                 private int too = 2
             }
-            """
-
-            @TupleConstructor(defaultsMode = DefaultsMode.AUTO, includeFields = true)
+        '''
+        assertScript shell, '''
+            @TupleConstructor(defaultsMode=DefaultsMode.AUTO, includeFields=true)
             class C {
                 String one = 'won'
                 int too = 2
@@ -405,8 +414,9 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
                 'public C(java.lang.String,int)',
                 'public C(int)'
             ].toSet()
-
-            @TupleConstructor(defaultsMode = DefaultsMode.ON, includeFields = true)
+        '''
+        assertScript shell, '''
+            @TupleConstructor(defaultsMode=DefaultsMode.ON, includeFields=true)
             class D {
                 String one = 'won'
                 int too = 2
@@ -422,22 +432,48 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
                 'public D()'
             ].toSet()
         '''
-        assertScript '''
-        import groovy.transform.*
-        @Canonical(defaultsMode=DefaultsMode.AUTO)
-        class Bar {
-            String a = 'a'
-            long b
-            Integer c = 24
-            short d
-            String e = 'e'
-        }
-
-        short one = 1
-        assert new Bar(3L, one).toString() == 'Bar(a, 3, 24, 1, e)'
-        assert new Bar('A', 3L, one).toString() == 'Bar(A, 3, 24, 1, e)'
-        assert new Bar('A', 3L, 42, one).toString() == 'Bar(A, 3, 42, 1, e)'
-        assert new Bar('A', 3L, 42, one, 'E').toString() == 'Bar(A, 3, 42, 1, E)'
+        assertScript shell, '''
+            @Canonical(defaultsMode=DefaultsMode.AUTO)
+            class Bar {
+                String a = 'a'
+                long b
+                Integer c = 24
+                short d
+                String e = 'e'
+            }
+
+            short one = 1
+            assert new Bar(3L, one).toString() == 'Bar(a, 3, 24, 1, e)'
+            assert new Bar('A', 3L, one).toString() == 'Bar(A, 3, 24, 1, e)'
+            assert new Bar('A', 3L, 42, one).toString() == 'Bar(A, 3, 42, 1, e)'
+            assert new Bar('A', 3L, 42, one, 'E').toString() == 'Bar(A, 3, 42, 1, E)'
+        '''
+    }
+
+    // GROOVY-10790
+    @Test
+    void testWithMapConstructor() {
+        assertScript shell, '''
+            @MapConstructor @TupleConstructor
+            @ToString
+            class Foo {
+                String bar, baz = 'z'
+            }
+
+            assert new Foo('x','y').toString() == 'Foo(x, y)'
+            assert new Foo('x').toString() == 'Foo(x, z)'
+            assert new Foo().toString() == 'Foo(null, z)'
+        '''
+        assertScript shell, ''' // multiple sources of no-arg constructor
+            @MapConstructor(noArg=true) @TupleConstructor
+            @ToString
+            class Foo {
+                String bar, baz = 'z'
+            }
+
+            assert new Foo('x','y').toString() == 'Foo(x, y)'
+            assert new Foo('x').toString() == 'Foo(x, z)'
+            assert new Foo().toString() == 'Foo(null, z)'
         '''
     }
 }