Copilot commented on code in PR #2810:
URL: https://github.com/apache/groovy/pull/2810#discussion_r3809063798


##########
src/main/java/groovy/util/ConfigObject.java:
##########
@@ -251,20 +253,20 @@ private void writeConfig(String prefix, ConfigObject map, 
BufferedWriter out, in
 
                     if (configSize == 1 || 
DefaultGroovyMethods.asBoolean(dotsInKeys)) {
                         if (firstSize == 1 && firstValue instanceof 
ConfigObject) {
-                            key = KEYWORDS.contains(key) ? 
FormatHelper.inspect(key) : key;
+                            key = renderKey(key);
                             String writePrefix = prefix + key + "." + firstKey 
+ ".";
                             writeConfig(writePrefix, (ConfigObject) 
firstValue, out, tab, true);

Review Comment:
   In the single-entry ConfigObject flattening branch, `firstKey` is appended 
into `writePrefix` without being rendered. That means a nested key like `a 
b`/`a.b` can still be written as source (and often won’t parse back), even 
though other paths now use `renderKey`. Also, if the rendered outer key starts 
with a quote and `prefix` is empty, `writePrefix` will start with a string 
literal and assignments emitted under it need a receiver (`this.`) to parse.



##########
src/main/java/groovy/util/ConfigObject.java:
##########
@@ -273,30 +275,100 @@ private void writeConfig(String prefix, ConfigObject 
map, BufferedWriter out, in
                     }
                 }
             } else {
-                writeValue(key, space, prefix, v, out);
+                writeValue(renderKey(key), space, prefix, v, out);
             }
         }
     }
 
-    private static void writeValue(String key, String space, String prefix, 
Object value, BufferedWriter out) throws IOException {
-//        key = key.indexOf('.') > -1 ? InvokerHelper.inspect(key) : key;
-        boolean isKeyword = KEYWORDS.contains(key);
-        key = isKeyword ? FormatHelper.inspect(key) : key;
-
-        if (!StringGroovyMethods.asBoolean(prefix) && isKeyword) prefix = 
"this.";
-        
out.append(space).append(prefix).append(key).append('=').append(FormatHelper.inspect(value));
+    /**
+     * Writes one entry, given a key path whose components have already been 
rendered.
+     *
+     * @param keyPath the rendered key path, such as {@code foo} or {@code 
foo.'a b'}
+     */
+    private static void writeValue(String keyPath, String space, String 
prefix, Object value, BufferedWriter out) throws IOException {
+        // A quoted key cannot open a statement on its own, so it needs a 
receiver, exactly as a
+        // keyword key has always done.
+        if (!StringGroovyMethods.asBoolean(prefix) && keyPath.startsWith("'")) 
prefix = "this.";
+        
out.append(space).append(prefix).append(keyPath).append('=').append(renderValue(value));
         out.newLine();
     }
 
     private void writeNode(String key, String space, int tab, ConfigObject 
value, BufferedWriter out) throws IOException {
-        key = KEYWORDS.contains(key) ? FormatHelper.inspect(key) : key;
-        out.append(space).append(key).append(" {");
+        out.append(space).append(renderKey(key)).append(" {");
         out.newLine();
         writeConfig("", value, out, tab + 1, true);
         out.append(space).append('}');
         out.newLine();
     }
 
+    /**
+     * Renders a key as it must appear in the written configuration: bare when 
it is a plain
+     * identifier, and as a quoted literal otherwise. A key which is not an 
identifier would
+     * otherwise be written as though it were source, and read back as 
whatever it happened to
+     * parse as.
+     *
+     * @param key the key to render
+     * @return the key as it should be written
+     */
+    private static String renderKey(String key) {
+        return isIdentifier(key) ? key : FormatHelper.inspect(key);
+    }
+
+    private static boolean isIdentifier(String key) {
+        if (key == null || key.isEmpty() || KEYWORDS.contains(key)) return 
false;
+        if (!Character.isJavaIdentifierStart(key.charAt(0))) return false;
+        for (int i = 1, n = key.length(); i < n; i += 1) {
+            if (!Character.isJavaIdentifierPart(key.charAt(i))) return false;
+        }
+        return true;
+    }
+
+    /**
+     * Renders a value as a literal which reads back as the same data.
+     *
+     * @param value the value to render
+     * @return the value as it should be written
+     */
+    private static String renderValue(Object value) {
+        return FormatHelper.inspect(asWritableData(value));
+    }
+
+    /**
+     * Converts a value into something {@link FormatHelper#inspect} renders as 
inert data.
+     * <p>
+     * A {@link CharSequence} which is not a {@code String} is rendered as a 
double quoted
+     * literal, in which a dollar is live, so its text is carried over to a 
{@code String} and
+     * rendered single quoted instead. A value of any other type without a 
literal form would be
+     * written as a bare {@code toString()}, which is not data at all, so its 
text is carried
+     * over in the same way. Numbers and booleans already write as themselves.
+     *
+     * @param value the value to convert
+     * @return a value whose rendering is data
+     */
+    private static Object asWritableData(Object value) {
+        if (value == null || value instanceof String || value instanceof 
Number || value instanceof Boolean) {
+            return value;
+        }
+        if (value instanceof CharSequence) {
+            return value.toString();
+        }
+        if (value instanceof Map<?, ?> map) {
+            Map<Object, Object> converted = new LinkedHashMap<>(map.size());
+            for (Map.Entry<?, ?> entry : map.entrySet()) {
+                converted.put(asWritableData(entry.getKey()), 
asWritableData(entry.getValue()));
+            }
+            return converted;
+        }
+        if (value instanceof Collection<?> collection) {
+            List<Object> converted = new ArrayList<>(collection.size());
+            for (Object element : collection) {
+                converted.add(asWritableData(element));
+            }
+            return converted;
+        }
+        return value.toString();
+    }

Review Comment:
   `asWritableData` currently treats Java arrays as “other type” and falls back 
to `value.toString()`. For arrays this is the JVM identity form (e.g. 
`[Ljava.lang.String;@...` / `[I@...`), which loses the actual elements and 
changes `writeTo` behavior vs `FormatHelper.inspect(array)` (which renders a 
list-like literal). Consider converting arrays to a `List` and recursively 
sanitizing their elements, similar to the `Collection` branch.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to