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

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


The following commit(s) were added to refs/heads/master by this push:
     new 165bfd12f1 GROOVY-11893: Standardise hooks for Groovy pretty printing
165bfd12f1 is described below

commit 165bfd12f1b2c25250378fe90413d268a692c32b
Author: Paul King <[email protected]>
AuthorDate: Wed Apr 1 00:57:19 2026 +1000

    GROOVY-11893: Standardise hooks for Groovy pretty printing
---
 .../lang/IncorrectClosureArgumentsException.java   |   4 +-
 src/main/java/groovy/lang/MetaClassImpl.java       |   4 +-
 src/main/java/groovy/lang/MetaMethod.java          |   6 +-
 .../groovy/reflection/CachedConstructor.java       |   2 +-
 .../groovy/runtime/ArrayGroovyMethods.java         | 136 +++++++++++++++++++++
 .../groovy/runtime/DefaultGroovyMethods.java       |  95 ++++++++++++++
 .../org/codehaus/groovy/runtime/FormatHelper.java  | 123 +++++++++++--------
 .../groovy/runtime/FormatHelperTest.groovy         |  71 +++++++++++
 .../org/apache/groovy/macrolib/MacroLibTest.groovy |   8 +-
 9 files changed, 385 insertions(+), 64 deletions(-)

diff --git a/src/main/java/groovy/lang/IncorrectClosureArgumentsException.java 
b/src/main/java/groovy/lang/IncorrectClosureArgumentsException.java
index ad09235ecb..dcb0a9d06f 100644
--- a/src/main/java/groovy/lang/IncorrectClosureArgumentsException.java
+++ b/src/main/java/groovy/lang/IncorrectClosureArgumentsException.java
@@ -38,9 +38,9 @@ public class IncorrectClosureArgumentsException extends 
GroovyRuntimeException {
             "Incorrect arguments to closure: "
                 + closure
                 + ". Expected: "
-                + FormatHelper.toString(expected)
+                + FormatHelper.toArrayString(expected)
                 + ", actual: "
-                + FormatHelper.toString(arguments));
+                + FormatHelper.toString(arguments));  // arguments is Object, 
not array
         this.closure = closure;
         this.arguments = arguments;
         this.expected = expected;
diff --git a/src/main/java/groovy/lang/MetaClassImpl.java 
b/src/main/java/groovy/lang/MetaClassImpl.java
index 0a2853ef97..3afaf40f47 100644
--- a/src/main/java/groovy/lang/MetaClassImpl.java
+++ b/src/main/java/groovy/lang/MetaClassImpl.java
@@ -3344,11 +3344,11 @@ public class MetaClassImpl implements MetaClass, 
MutableMetaClass {
         StringBuilder msg = new StringBuilder("Ambiguous method overloading 
for method ");
         msg.append(theClassName).append("#").append(name)
                 .append(".\nCannot resolve which method to invoke for ")
-                .append(FormatHelper.toString(arguments))
+                .append(FormatHelper.toArrayString(arguments))
                 .append(" due to overlapping prototypes between:");
         for (final Object match : matches) {
             CachedClass[] types = ((ParameterTypes) match).getParameterTypes();
-            msg.append("\n\t").append(FormatHelper.toString(types));
+            msg.append("\n\t").append(FormatHelper.toArrayString(types));
         }
         return msg.toString();
     }
diff --git a/src/main/java/groovy/lang/MetaMethod.java 
b/src/main/java/groovy/lang/MetaMethod.java
index e8d8cb6849..99f92a9da4 100644
--- a/src/main/java/groovy/lang/MetaMethod.java
+++ b/src/main/java/groovy/lang/MetaMethod.java
@@ -94,9 +94,9 @@ public abstract class MetaMethod extends ParameterTypes 
implements MetaMember, C
                     "Parameters to method: "
                     + getName()
                     + " do not match types: "
-                    + FormatHelper.toString(getParameterTypes())
+                    + FormatHelper.toArrayString(getParameterTypes())
                     + " for arguments: "
-                    + FormatHelper.toString(arguments));
+                    + FormatHelper.toArrayString(arguments));
         }
     }
 
@@ -143,7 +143,7 @@ public abstract class MetaMethod extends ParameterTypes 
implements MetaMember, C
             + "[name: "
             + getName()
             + " params: "
-            + FormatHelper.toString(getParameterTypes())
+            + FormatHelper.toArrayString(getParameterTypes())
             + " returns: "
             + getReturnType()
             + " owner: "
diff --git 
a/src/main/java/org/codehaus/groovy/reflection/CachedConstructor.java 
b/src/main/java/org/codehaus/groovy/reflection/CachedConstructor.java
index 6d23d52394..ff9021e6d4 100644
--- a/src/main/java/org/codehaus/groovy/reflection/CachedConstructor.java
+++ b/src/main/java/org/codehaus/groovy/reflection/CachedConstructor.java
@@ -112,7 +112,7 @@ public class CachedConstructor extends ParameterTypes 
implements MetaMember {
                 init
                         + constructor
                         + " with arguments: "
-                        + FormatHelper.toString(argumentArray)
+                        + FormatHelper.toArrayString(argumentArray)
                         + " reason: "
                         + e,
                 setReason ? e : null);
diff --git a/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java 
b/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java
index 15690cfd2c..b734bff5d7 100644
--- a/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java
@@ -4429,6 +4429,142 @@ public class ArrayGroovyMethods extends 
DefaultGroovyMethodsSupport {
         return DefaultGroovyMethods.groupByMany(Arrays.asList(self), valueFn, 
keyFn);
     }
 
+    
//--------------------------------------------------------------------------
+    // groovyToString
+
+    /**
+     * Returns Groovy's list-like string representation for an Object array.
+     * This is used by Groovy's formatting infrastructure (e.g., GString 
interpolation,
+     * {@code println}, assert messages). By default, it delegates to
+     * {@link FormatHelper#toArrayString(Object[])}.
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(Object[])},
+     * the JDK's default array {@code toString()} is used instead.
+     * Alternatively, you have the option to provide a replacement extension 
method in an extension module
+     * to customize how arrays are displayed throughout Groovy.
+     *
+     * @param self the array to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(Object[] self) {
+        return FormatHelper.toArrayString(self);
+    }
+
+    /**
+     * Returns Groovy's list-like string representation for a boolean array.
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(boolean[])},
+     * the JDK's default array {@code toString()} is used instead.
+     *
+     * @param self the array to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(boolean[] self) {
+        return 
FormatHelper.toListString(DefaultTypeTransformation.primitiveArrayToList(self));
+    }
+
+    /**
+     * Returns Groovy's list-like string representation for a byte array.
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(byte[])},
+     * the JDK's default array {@code toString()} is used instead.
+     *
+     * @param self the array to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(byte[] self) {
+        return 
FormatHelper.toListString(DefaultTypeTransformation.primitiveArrayToList(self));
+    }
+
+    /**
+     * Returns Groovy's string representation for a char array.
+     * By default, a char array is rendered as a String (e.g. {@code 
'abc'.chars}
+     * displays as {@code abc}).
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(char[])},
+     * the JDK's default array {@code toString()} is used instead.
+     *
+     * @param self the array to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(char[] self) {
+        return new String(self);
+    }
+
+    /**
+     * Returns Groovy's list-like string representation for a short array.
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(short[])},
+     * the JDK's default array {@code toString()} is used instead.
+     *
+     * @param self the array to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(short[] self) {
+        return 
FormatHelper.toListString(DefaultTypeTransformation.primitiveArrayToList(self));
+    }
+
+    /**
+     * Returns Groovy's list-like string representation for an int array.
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(int[])},
+     * the JDK's default array {@code toString()} is used instead.
+     *
+     * @param self the array to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(int[] self) {
+        return 
FormatHelper.toListString(DefaultTypeTransformation.primitiveArrayToList(self));
+    }
+
+    /**
+     * Returns Groovy's list-like string representation for a long array.
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(long[])},
+     * the JDK's default array {@code toString()} is used instead.
+     *
+     * @param self the array to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(long[] self) {
+        return 
FormatHelper.toListString(DefaultTypeTransformation.primitiveArrayToList(self));
+    }
+
+    /**
+     * Returns Groovy's list-like string representation for a float array.
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(float[])},
+     * the JDK's default array {@code toString()} is used instead.
+     *
+     * @param self the array to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(float[] self) {
+        return 
FormatHelper.toListString(DefaultTypeTransformation.primitiveArrayToList(self));
+    }
+
+    /**
+     * Returns Groovy's list-like string representation for a double array.
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(double[])},
+     * the JDK's default array {@code toString()} is used instead.
+     *
+     * @param self the array to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(double[] self) {
+        return 
FormatHelper.toListString(DefaultTypeTransformation.primitiveArrayToList(self));
+    }
+
     
//--------------------------------------------------------------------------
     // head
 
diff --git 
a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java 
b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
index 0044eaf6b6..d52bbbb223 100644
--- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
@@ -8139,6 +8139,101 @@ public class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
         return answer;
     }
 
+    
//--------------------------------------------------------------------------
+    // groovyToString
+
+    /**
+     * Returns Groovy's default string representation for a Map.
+     * This is used by Groovy's formatting infrastructure (e.g., GString 
interpolation,
+     * {@code println}, assert messages). By default, it delegates to
+     * {@link FormatHelper#toMapString(Map)}.
+     * <p>
+     * <pre class="groovyTestCase">
+     * assert [a:1, b:2].groovyToString() == '[a:1, b:2]'
+     * </pre>
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(Map)},
+     * the normal map {@code toString()} is used instead.
+     * Alternatively, you have the option to provide a replacement extension 
method in an extension module
+     * to customize how maps are displayed throughout Groovy.
+     *
+     * @param self the Map to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(Map self) {
+        return FormatHelper.toMapString(self);
+    }
+
+    /**
+     * Returns Groovy's default string representation for a Range.
+     * By default, it delegates to {@link Range#toString()},
+     * producing the compact {@code from..to} notation.
+     * <p>
+     * <pre class="groovyTestCase">
+     * assert (1..4).groovyToString() == '1..4'
+     * </pre>
+     * <p>
+     * This method exists to stop the {@code groovyToString(Collection)} 
variant
+     * from overriding the built-in {@code Range#toString}.
+     * Since a range is a list, you can use {@code range.toListString()}
+     * to print it using normal list formatting.
+     *
+     * @param self the Range to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(Range self) {
+        return self.toString();
+    }
+
+    /**
+     * Returns Groovy's default string representation for a Collection.
+     * This is used by Groovy's formatting infrastructure (e.g., GString 
interpolation,
+     * {@code println}, assert messages). By default, it delegates to
+     * {@link FormatHelper#toListString(Collection)}.
+     * <p>
+     * <pre class="groovyTestCase">
+     * assert [1, 2, 3].groovyToString() == '[1, 2, 3]'
+     * </pre>
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(Collection)},
+     * the normal list {@code toString()} is used instead.
+     * Alternatively, you have the option to provide a replacement extension 
method in an extension module
+     * to customize how collections are displayed throughout Groovy.
+     *
+     * @param self the Collection to format
+     * @return the string representation
+     * @since 6.0.0
+     */
+    public static String groovyToString(Collection self) {
+        return FormatHelper.toListString(self);
+    }
+
+    /**
+     * Returns Groovy's default string representation for an XML Element.
+     * This is used by Groovy's formatting infrastructure. By default, it
+     * serializes the element using {@code 
groovy.xml.XmlUtil.serialize(Element)}.
+     * <p>
+     * If disabled, e.g. via {@code 
-Dgroovy.extension.disable=groovyToString(Element)},
+     * the element's default {@code toString()} is used instead.
+     * Alternatively, you have the option to provide a replacement extension 
method in an extension module
+     * to customize how elements are displayed throughout Groovy.
+     *
+     * @param self the Element to format
+     * @return the serialized XML string
+     * @since 6.0.0
+     */
+    public static String groovyToString(org.w3c.dom.Element self) {
+        try {
+            java.lang.reflect.Method serialize = 
Class.forName("groovy.xml.XmlUtil")
+                    .getMethod("serialize", org.w3c.dom.Element.class);
+            return (String) serialize.invoke(null, self);
+        } catch (Exception e) {
+            return self.toString();
+        }
+    }
+
     
//--------------------------------------------------------------------------
     // hasProperty
 
diff --git a/src/main/java/org/codehaus/groovy/runtime/FormatHelper.java 
b/src/main/java/org/codehaus/groovy/runtime/FormatHelper.java
index 30f8d7d3a4..6ed703c0e0 100644
--- a/src/main/java/org/codehaus/groovy/runtime/FormatHelper.java
+++ b/src/main/java/org/codehaus/groovy/runtime/FormatHelper.java
@@ -21,6 +21,7 @@ package org.codehaus.groovy.runtime;
 import groovy.lang.GroovyRuntimeException;
 import groovy.lang.GroovySystem;
 import groovy.lang.MetaClassRegistry;
+import groovy.lang.MetaMethod;
 import groovy.lang.Range;
 import groovy.lang.Writable;
 import groovy.transform.NamedParam;
@@ -28,15 +29,12 @@ import groovy.transform.NamedParams;
 import org.apache.groovy.io.StringBuilderWriter;
 import org.codehaus.groovy.control.ResolveVisitor;
 import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
-import org.w3c.dom.Element;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.io.Writer;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Map;
@@ -61,8 +59,25 @@ public class FormatHelper {
     private static final int ITEM_ALLOCATE_SIZE = 5;
 
     public static final MetaClassRegistry metaRegistry = 
GroovySystem.getMetaClassRegistry();
-    private static final String XMLUTIL_CLASS_FULL_NAME = "groovy.xml.XmlUtil";
-    private static final String SERIALIZE_METHOD_NAME = "serialize";
+    private static final String GROOVY_TO_STRING = "groovyToString";
+    private static final Class[] EMPTY_TYPES = {};
+
+    /**
+     * Attempts to invoke a {@code groovyToString()} extension method on the 
given object
+     * via the metaclass. Returns the result if found, or {@code null} if no 
such method exists.
+     */
+    static String tryGroovyToString(Object object) {
+        if (object == null) return null;
+        try {
+            MetaMethod method = 
InvokerHelper.getMetaClass(object).getMetaMethod(GROOVY_TO_STRING, EMPTY_TYPES);
+            if (method != null) {
+                return (String) method.invoke(object, EMPTY_ARGS);
+            }
+        } catch (ClassCastException | GroovyRuntimeException ignore) {
+            // method found but not applicable to this object's actual type
+        }
+        return null;
+    }
 
     static final Set<String> DEFAULT_IMPORT_PKGS = new HashSet<>();
     static final Set<String> DEFAULT_IMPORT_CLASSES = new HashSet<>();
@@ -152,41 +167,43 @@ public class FormatHelper {
         }
         if (arguments.getClass().isArray()) {
             if (arguments instanceof Object[]) {
+                if (!inspect && !escapeBackslashes && maxSize == -1 && !safe) {
+                    String result = tryGroovyToString(arguments);
+                    return result != null ? result : arguments.toString();
+                }
                 return toArrayString((Object[]) arguments, inspect, 
escapeBackslashes, maxSize, safe);
             }
+            if (!inspect && !escapeBackslashes && maxSize == -1 && !safe) {
+                // char[] and primitive arrays — delegate to groovyToString if 
available
+                String result = tryGroovyToString(arguments);
+                return result != null ? result : arguments.toString();
+            }
             if (arguments instanceof char[]) {
                 return new String((char[]) arguments);
             }
             // other primitives
             return 
formatCollection(DefaultTypeTransformation.arrayAsCollection(arguments), 
inspect, escapeBackslashes, maxSize, safe);
         }
-        if (arguments instanceof Range range) {
-            try {
-                if (inspect) {
-                    return range.inspect();
-                } else {
-                    return range.toString();
+        // When inspect/maxSize/safe are active, use the hardcoded formatters 
for
+        // Range, Collection, and Map (they support those parameters). 
Otherwise, the
+        // groovyToString check below will handle them.
+        if (inspect || escapeBackslashes || maxSize != -1 || safe) {
+            if (arguments instanceof Range range) {
+                try {
+                    return inspect ? range.inspect() : range.toString();
+                } catch (RuntimeException ex) {
+                    if (!safe) throw ex;
+                    return handleFormattingException(arguments, ex);
+                } catch (Exception ex) {
+                    if (!safe) throw new GroovyRuntimeException(ex);
+                    return handleFormattingException(arguments, ex);
                 }
-            } catch (RuntimeException ex) {
-                if (!safe) throw ex;
-                return handleFormattingException(arguments, ex);
-            } catch (Exception ex) {
-                if (!safe) throw new GroovyRuntimeException(ex);
-                return handleFormattingException(arguments, ex);
             }
-        }
-        if (arguments instanceof Collection) {
-            return formatCollection((Collection) arguments, inspect, 
escapeBackslashes, maxSize, safe);
-        }
-        if (arguments instanceof Map) {
-            return formatMap((Map) arguments, inspect, escapeBackslashes, 
maxSize, safe);
-        }
-        if (arguments instanceof Element) {
-            try {
-                Method serialize = 
Class.forName(XMLUTIL_CLASS_FULL_NAME).getMethod(SERIALIZE_METHOD_NAME, 
Element.class);
-                return (String) serialize.invoke(null, arguments);
-            } catch (ClassNotFoundException | IllegalAccessException | 
InvocationTargetException | NoSuchMethodException e) {
-                throw new RuntimeException(e);
+            if (arguments instanceof Collection) {
+                return formatCollection((Collection) arguments, inspect, 
escapeBackslashes, maxSize, safe);
+            }
+            if (arguments instanceof Map) {
+                return formatMap((Map) arguments, inspect, escapeBackslashes, 
maxSize, safe);
             }
         }
         if (arguments instanceof CharSequence) {
@@ -198,9 +215,13 @@ public class FormatHelper {
             if (!inspect) return arg;
             return !escapeBackslashes && multiline(arg) ? "\"\"\"" + arg + 
"\"\"\"" : DQ + arg.replace(DQ, "\\\"") + DQ;
         }
+        // Check for a groovyToString() extension method. By default, DGM 
methods
+        // provide Groovy's custom formatting for Map, Collection, and 
Object[].
+        // Users can add groovyToString for any type via extension modules, or
+        // disable it with -Dgroovy.extension.disable=groovyToString.
+        String groovyStr = tryGroovyToString(arguments);
+        if (groovyStr != null) return groovyStr;
         try {
-            // TODO: For GROOVY-2599 do we need something like below but it 
breaks other things
-//            return (String) invokeMethod(arguments, "toString", EMPTY_ARGS);
             return arguments.toString();
         } catch (RuntimeException ex) {
             if (!safe) throw ex;
@@ -482,14 +503,6 @@ public class FormatHelper {
     public static void write(Writer out, Object object) throws IOException {
         if (object instanceof String) {
             out.write((String) object);
-        } else if (object instanceof Object[]) {
-            out.write(toArrayString((Object[]) object));
-        } else if (object instanceof Map) {
-            out.write(toMapString((Map) object));
-        } else if (object instanceof Collection) {
-            out.write(toListString((Collection) object));
-        } else if (object instanceof Writable writable) {
-            writable.writeTo(out);
         } else if (object instanceof InputStream || object instanceof Reader) {
             // Copy stream to stream
             Reader reader;
@@ -506,7 +519,14 @@ public class FormatHelper {
                 }
             }
         } else {
-            out.write(toString(object));
+            String result = tryGroovyToString(object);
+            if (result != null) {
+                out.write(result);
+            } else if (object instanceof Writable writable) {
+                writable.writeTo(out);
+            } else {
+                out.write(toString(object));
+            }
         }
     }
 
@@ -516,16 +536,6 @@ public class FormatHelper {
     public static void append(Appendable out, Object object) throws 
IOException {
         if (object instanceof String) {
             out.append((String) object);
-        } else if (object instanceof Object[]) {
-            out.append(toArrayString((Object[]) object));
-        } else if (object instanceof Map) {
-            out.append(toMapString((Map) object));
-        } else if (object instanceof Collection) {
-            out.append(toListString((Collection) object));
-        } else if (object instanceof Writable writable) {
-            Writer stringWriter = new StringBuilderWriter();
-            writable.writeTo(stringWriter);
-            out.append(stringWriter.toString());
         } else if (object instanceof InputStream || object instanceof Reader) {
             // Copy stream to stream
             try (Reader reader =
@@ -540,7 +550,16 @@ public class FormatHelper {
                 }
             }
         } else {
-            out.append(toString(object));
+            String result = tryGroovyToString(object);
+            if (result != null) {
+                out.append(result);
+            } else if (object instanceof Writable writable) {
+                Writer stringWriter = new StringBuilderWriter();
+                writable.writeTo(stringWriter);
+                out.append(stringWriter.toString());
+            } else {
+                out.append(toString(object));
+            }
         }
     }
 }
diff --git 
a/src/test/groovy/org/codehaus/groovy/runtime/FormatHelperTest.groovy 
b/src/test/groovy/org/codehaus/groovy/runtime/FormatHelperTest.groovy
index ca9e3c1c55..ba3dade169 100644
--- a/src/test/groovy/org/codehaus/groovy/runtime/FormatHelperTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/runtime/FormatHelperTest.groovy
@@ -364,4 +364,75 @@ class FormatHelperTest {
     void testMetaRegistryNotNull() {
         assertNotNull(FormatHelper.metaRegistry)
     }
+
+    // -- groovyToString tests (GROOVY-11893) --
+
+    static class Foo {
+        String toString() { 'some foo' }
+        String groovyToString() { 'some bar' }
+    }
+
+    @Test
+    void groovyToStringUsedInMapFormatting() {
+        assert [foo: new Foo()].toString() == '[foo:some bar]'
+    }
+
+    @Test
+    void groovyToStringUsedInListFormatting() {
+        assert [new Foo()].toString() == '[some bar]'
+    }
+
+    @Test
+    void groovyToStringUsedInInterpolation() {
+        def f = new Foo()
+        assert "$f" == 'some bar'
+    }
+
+    @Test
+    void groovyToStringUsedByFormatHelper() {
+        assert FormatHelper.toString(new Foo()) == 'some bar'
+    }
+
+    static class BadFoo {
+        Date groovyToString() { new Date() }
+        String toString() { 'fallback foo' }
+    }
+
+    @Test
+    void groovyToStringWrongReturnTypeFallsBack() {
+        assert FormatHelper.toString(new BadFoo()) == 'fallback foo'
+    }
+
+    @Test
+    void groovyToStringDisabledViaForkedJvm() {
+        def groovyHome = System.getProperty('groovy.home') ?: 
System.env.GROOVY_HOME
+        // Find java executable
+        def javaHome = System.getProperty('java.home')
+        def java = new File(javaHome, 'bin/java').absolutePath
+        // Build classpath from current test classpath
+        def cp = System.getProperty('java.class.path')
+
+        def script = '''
+            int[] arr = [1, 2, 3]
+            // With groovyToString disabled for int[], should fall back to 
Java's toString
+            // which produces something like [I@hashcode rather than [1, 2, 3]
+            print(arr.toString().startsWith('[1'))
+        '''
+
+        def scriptFile = File.createTempFile('groovyToStringTest', '.groovy')
+        scriptFile.deleteOnExit()
+        scriptFile.text = script
+
+        def pb = new ProcessBuilder(java, '-cp', cp,
+                '-Dgroovy.extension.disable=groovyToString(int[])',
+                'groovy.ui.GroovyMain', scriptFile.absolutePath)
+        pb.redirectErrorStream(true)
+        def process = pb.start()
+        def output = process.inputStream.text.trim()
+        def exitCode = process.waitFor()
+
+        // With groovyToString disabled, int[].toString() should NOT produce 
[1, 2, 3]
+        assert output == 'false', "Expected groovyToString to be disabled, but 
got: $output"
+        assert exitCode == 0
+    }
 }
diff --git 
a/subprojects/groovy-macro-library/src/test/groovy/org/apache/groovy/macrolib/MacroLibTest.groovy
 
b/subprojects/groovy-macro-library/src/test/groovy/org/apache/groovy/macrolib/MacroLibTest.groovy
index 95e110ab92..2f67ecc1e5 100644
--- 
a/subprojects/groovy-macro-library/src/test/groovy/org/apache/groovy/macrolib/MacroLibTest.groovy
+++ 
b/subprojects/groovy-macro-library/src/test/groovy/org/apache/groovy/macrolib/MacroLibTest.groovy
@@ -34,7 +34,7 @@ final class MacroLibTest {
     @Test
     void testSV() {
         assertScript BASE + '''\
-            assert SV(num, list, range, string).toString() == 'num=42, 
list=[1, 2, 3], range=[0, 1, 2, 3, 4, 5], string=foo'
+            assert SV(num, list, range, string).toString() == 'num=42, 
list=[1, 2, 3], range=0..5, string=foo'
         '''
     }
 
@@ -44,14 +44,14 @@ final class MacroLibTest {
             def cl = {
                 SV(num, list, range, string).toString()
             }
-            assert cl().toString() == 'num=42, list=[1, 2, 3], range=[0, 1, 2, 
3, 4, 5], string=foo'
+            assert cl().toString() == 'num=42, list=[1, 2, 3], range=0..5, 
string=foo'
         '''
     }
 
     @Test
     void testList() {
         assertScript BASE + '''\
-            assert [SV(num, list), SV(range, string)].toString() == '[num=42, 
list=[1, 2, 3], range=[0, 1, 2, 3, 4, 5], string=foo]'
+            assert [SV(num, list), SV(range, string)].toString() == '[num=42, 
list=[1, 2, 3], range=0..5, string=foo]'
         '''
     }
 
@@ -59,7 +59,7 @@ final class MacroLibTest {
     void testSVInclude() {
         assertScript BASE + '''\
             def numSV = SV(num)
-            assert SV(numSV, list, range, string).toString() == 'numSV=num=42, 
list=[1, 2, 3], range=[0, 1, 2, 3, 4, 5], string=foo'
+            assert SV(numSV, list, range, string).toString() == 'numSV=num=42, 
list=[1, 2, 3], range=0..5, string=foo'
         '''
     }
 

Reply via email to