Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
46993e482c Merge main into claude/fix-yaml-stringify-spacing 2025-11-03 10:52:14 +00:00
Claude Bot
8692c36301 fix: YAML.stringify spacing for multiline values (#23501)
Fixed two issues with YAML.stringify output formatting:

1. Removed extra space after colon when value is on next line
   - Before: `key: \n  value`
   - After: `key:\n  value`

2. Keep empty arrays/objects inline instead of newline
   - Before: `arr: \n  []`
   - After: `arr: []`

The fix checks if a value is an empty array or object before deciding
whether to put it on a newline. Empty collections stay inline with
proper spacing, while non-empty collections go on the next line
without a space after the colon.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 23:47:01 +00:00
3 changed files with 157 additions and 76 deletions

View File

@@ -477,12 +477,17 @@ const Stringifier = struct {
first = false;
this.appendString(prop_name);
this.builder.append(.latin1, ": ");
const unwrapped_value = try iter.value.unwrapBoxedPrimitive(global);
const needs_newline = propValueNeedsNewline(global, unwrapped_value);
this.indent += 1;
if (propValueNeedsNewline(iter.value)) {
if (needs_newline) {
this.builder.append(.lchar, ':');
this.newline();
} else {
this.builder.append(.latin1, ": ");
}
try this.stringify(global, iter.value);
@@ -492,9 +497,31 @@ const Stringifier = struct {
}
}
/// Does this object property value need a newline? True for arrays and objects.
fn propValueNeedsNewline(value: JSValue) bool {
return !value.isNumber() and !value.isBoolean() and !value.isNull() and !value.isString();
/// Does this object property value need a newline? True for non-empty arrays and objects.
fn propValueNeedsNewline(global: *JSGlobalObject, unwrapped_value: JSValue) bool {
// Primitives don't need newlines
if (unwrapped_value.isNumber() or unwrapped_value.isBoolean() or unwrapped_value.isNull() or unwrapped_value.isString()) {
return false;
}
// Check if it's an empty array - these should stay inline
if (unwrapped_value.isArray()) {
const len = unwrapped_value.getLength(global) catch 0;
return len > 0;
}
// Check if it's an empty object - these should stay inline
if (unwrapped_value.isObject()) {
// Try to iterate and see if there are any properties
var iter: jsc.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true }) = (jsc.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true }).init(
global,
unwrapped_value.toObject(global) catch return true,
)) catch return true;
defer iter.deinit();
return iter.len > 0;
}
return true;
}
fn newline(this: *Stringifier) void {

View File

@@ -910,7 +910,7 @@ my_config:
port: 5432,
},
};
expect(YAML.stringify(obj, null, 2)).toBe("database: \n host: localhost\n port: 5432");
expect(YAML.stringify(obj, null, 2)).toBe("database:\n host: localhost\n port: 5432");
});
test("stringifies mixed structures", () => {
@@ -921,7 +921,7 @@ my_config:
],
};
const expected =
"users: \n - name: Alice\n hobbies: \n - reading\n - hiking\n - name: Bob\n hobbies: \n - gaming";
"users:\n - name: Alice\n hobbies:\n - reading\n - hiking\n - name: Bob\n hobbies:\n - gaming";
expect(YAML.stringify(obj, null, 2)).toBe(expected);
});
@@ -1461,7 +1461,7 @@ my_config:
str: new String("world"),
bool: new Boolean(false),
};
expect(YAML.stringify(obj, null, 2)).toBe("num: \n 3.14\nstr: world\nbool: \n false");
expect(YAML.stringify(obj, null, 2)).toBe("num: 3.14\nstr: world\nbool: false");
});
test("handles Date objects", () => {
@@ -1473,7 +1473,7 @@ my_config:
// In objects
const obj = { created: date };
expect(YAML.stringify(obj, null, 2)).toBe("created: \n {}");
expect(YAML.stringify(obj, null, 2)).toBe("created: {}");
});
test("handles RegExp objects", () => {
@@ -1482,7 +1482,7 @@ my_config:
expect(YAML.stringify(regex)).toBe("{}");
const obj = { pattern: regex };
expect(YAML.stringify(obj, null, 2)).toBe("pattern: \n {}");
expect(YAML.stringify(obj, null, 2)).toBe("pattern: {}");
});
test("handles Error objects", () => {
@@ -1685,11 +1685,11 @@ my_config:
const yaml1 = YAML.stringify(obj1, null, 2);
expect(yaml1).toMatchInlineSnapshot(`
"data:
"data:
&data
value: shared
nested:
data:
nested:
data:
*data"
`);
@@ -1719,25 +1719,25 @@ nested:
const yaml2 = YAML.stringify(obj2, null, 2);
expect(yaml2).toMatchInlineSnapshot(`
"item:
"item:
&item
type: A
nested1:
item:
nested1:
item:
*item
other:
item:
other:
item:
&item1
type: B
nested2:
item:
nested2:
item:
*item1
sub:
item:
sub:
item:
&item2
type: C
refs:
item:
refs:
item:
*item2"
`);
@@ -1808,7 +1808,7 @@ refs:
const yaml2 = YAML.stringify(complex, null, 2);
expect(yaml2).toMatchInlineSnapshot(`
"arrays:
"arrays:
- &item0
- 1
- 2
@@ -1816,8 +1816,8 @@ refs:
- 3
- 4
- *item0
nested:
moreArrays:
nested:
moreArrays:
- &item2
- 5
- 6
@@ -1852,18 +1852,18 @@ nested:
const yaml = YAML.stringify(mixed, null, 2);
expect(yaml).toMatchInlineSnapshot(`
"item:
"item:
&item
type: object
items:
items:
- &item0
- array
- &item1
nested: obj
- *item0
- *item1
refs:
item:
refs:
item:
*item"
`);
@@ -1893,18 +1893,16 @@ refs:
const yaml = YAML.stringify(obj, null, 2);
expect(yaml).toMatchInlineSnapshot(`
""":
""":
&value0
empty: key
nested:
"":
nested:
"":
*value0
another:
"":
&value1
another:
"": &value1
{}
what:
*value1"
what: *value1"
`);
// Since empty names can't be used as anchors, they get a counter
@@ -1948,38 +1946,38 @@ refs:
const yaml = YAML.stringify(complex, null, 2);
expect(yaml).toMatchInlineSnapshot(`
"data:
"data:
&data
id: 0
level1:
data:
level1:
data:
*data
sub1:
data:
sub1:
data:
&data1
id: 1
sub2:
data:
sub2:
data:
*data1
level2:
data:
level2:
data:
&data2
id: 2
nested:
data:
nested:
data:
&data3
id: 3
deep:
data:
deep:
data:
&data4
id: 4
refs:
data:
refs:
data:
*data2
all:
- data:
all:
- data:
*data3
- data:
- data:
*data4"
`);
@@ -2020,13 +2018,11 @@ refs:
obj.root2 = root;
expect(YAML.stringify(obj, null, 2)).toMatchInlineSnapshot(`
"&root
cycle:
cycle:
*root
root:
&root1
root: &root1
{}
root2:
*root1"
root2: *root1"
`);
});
});
@@ -2284,22 +2280,16 @@ refs:
const yaml = YAML.stringify(nested, null, 2);
expect(yaml).toMatchInlineSnapshot(`
"emptyObj:
{}
emptyArr:
[]
nested:
deepEmpty:
{}
deepArr:
[]
mixed:
"emptyObj: {}
emptyArr: []
nested:
deepEmpty: {}
deepArr: []
mixed:
- {}
- []
- inner:
{}
- inner:
[]"
- inner: {}
- inner: []"
`);
});

View File

@@ -0,0 +1,64 @@
import { expect, test } from "bun:test";
test("issue #23501: YAML.stringify should not add extra spaces for multiline values", () => {
const data = {
arr: [],
nested: {
obj: "str",
},
};
const result = Bun.YAML.stringify(data, null, 2);
// Issue 1: No space after colon when value is on next line
// Issue 2: Empty arrays should stay inline as "arr: []"
expect(result).toBe("arr: []\nnested:\n obj: str");
});
test("issue #23501: YAML.stringify handles various empty collections correctly", () => {
const data = {
emptyArray: [],
emptyObject: {},
nonEmptyArray: [1, 2],
nonEmptyObject: { key: "value" },
str: "hello",
num: 42,
};
const result = Bun.YAML.stringify(data, null, 2);
// Empty arrays and objects should be inline with space after colon
expect(result).toContain("emptyArray: []");
expect(result).toContain("emptyObject: {}");
expect(result).toContain("str: hello");
expect(result).toContain("num: 42");
// Non-empty arrays and objects should be multiline without space after colon
expect(result).toContain("nonEmptyArray:\n");
expect(result).toContain("nonEmptyObject:\n");
});
test("issue #23501: YAML.stringify preserves correct formatting for nested structures", () => {
const data = {
users: [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
],
config: {
timeout: 5000,
retries: 3,
},
};
const result = Bun.YAML.stringify(data, null, 2);
// Arrays and objects with content should have colon without space, then newline
expect(result).toContain("users:\n");
expect(result).toContain("config:\n");
// Primitive values should have ": " (colon with space)
expect(result).toContain("timeout: 5000");
expect(result).toContain("retries: 3");
expect(result).toContain("name: Alice");
expect(result).toContain("age: 30");
});