Compare commits

...

6 Commits

Author SHA1 Message Date
Alistair Smith
13e097ea07 Merge branch 'main' into claude/fix-jsx-null-braces 2025-09-26 19:31:55 -07:00
Claude Bot
4425227803 test: update inspect.test.js for JSX attribute formatting changes
Update expected output to match new JSX attribute formatting where
non-string values (numbers, booleans) are wrapped in braces.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-27 02:07:41 +00:00
Alistair Smith
68408fbe42 Merge branch 'main' into claude/fix-jsx-null-braces 2025-09-22 21:58:30 -07:00
autofix-ci[bot]
ff606bd2fb [autofix.ci] apply automated fixes 2025-09-23 04:14:09 +00:00
Claude Bot
89066c0d3a test: add tests for JSX attribute formatting with non-string values
Added comprehensive tests to verify that JSX attributes with null and other
non-string values are properly wrapped in braces when printed to console.

Also includes tests for proper self-closing tag rendering with empty children arrays.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 04:11:22 +00:00
Claude Bot
4367229977 fix: wrap null and other non-string JSX attribute values in braces
Fixed JSX pretty printing to output valid JSX syntax by wrapping non-string
attribute values (null, undefined, booleans, numbers, objects, arrays, functions)
in braces.

Before: <Component params=null value=42 flag=true />
After:  <Component params={null} value={42} flag={true} />

Also fixed an issue where JSX elements with empty children arrays were
not printing the self-closing tag properly.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 04:06:34 +00:00
4 changed files with 224 additions and 9 deletions

View File

@@ -3138,7 +3138,12 @@ pub const Formatter = struct {
.{prop.trunc(128)},
);
if (tag.cell.isStringLike()) {
// For non-string values, wrap in braces for valid JSX syntax
const needs_braces = !tag.cell.isStringLike();
if (needs_braces) {
writer.writeAll("{");
} else {
if (comptime enable_ansi_colors) {
writer.writeAll(comptime Output.prettyFmt("<r><green>", true));
}
@@ -3146,7 +3151,9 @@ pub const Formatter = struct {
try this.format(tag, Writer, writer_, property_value, this.globalThis, enable_ansi_colors);
if (tag.cell.isStringLike()) {
if (needs_braces) {
writer.writeAll("}");
} else {
if (comptime enable_ansi_colors) {
writer.writeAll(comptime Output.prettyFmt("<r>", true));
}
@@ -3180,6 +3187,7 @@ pub const Formatter = struct {
};
if (print_children and !this.single_line) {
var did_print_children = false;
print_children: {
switch (tag.tag) {
.String => {
@@ -3188,6 +3196,7 @@ pub const Formatter = struct {
if (comptime enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("<r>", true));
writer.writeAll(">");
did_print_children = true;
if (children_string.len < 128) {
writer.writeString(children_string);
} else {
@@ -3202,6 +3211,7 @@ pub const Formatter = struct {
},
.JSX => {
writer.writeAll(">\n");
did_print_children = true;
{
this.indent += 1;
@@ -3217,6 +3227,7 @@ pub const Formatter = struct {
const length = try children.getLength(this.globalThis);
if (length == 0) break :print_children;
writer.writeAll(">\n");
did_print_children = true;
{
this.indent += 1;
@@ -3257,7 +3268,7 @@ pub const Formatter = struct {
writer.writeAll(">");
}
return;
if (did_print_children) return;
}
}
}

View File

@@ -1590,7 +1590,14 @@ pub const JestPrettyFormat = struct {
.{prop.trunc(128)},
);
if (tag.cell.isStringLike()) {
// For non-string values, wrap in braces for valid JSX syntax
// Check if the value is a string type (String or StringPossiblyFormatted)
const is_string = tag.tag == .String or tag.tag == .StringPossiblyFormatted;
const needs_braces = !is_string;
if (needs_braces) {
writer.writeAll("{");
} else {
if (comptime enable_ansi_colors) {
writer.writeAll(comptime Output.prettyFmt("<r><green>", true));
}
@@ -1598,7 +1605,9 @@ pub const JestPrettyFormat = struct {
try this.format(tag, Writer, writer_, property_value, this.globalThis, enable_ansi_colors);
if (tag.cell.isStringLike()) {
if (needs_braces) {
writer.writeAll("}");
} else {
if (comptime enable_ansi_colors) {
writer.writeAll(comptime Output.prettyFmt("<r>", true));
}
@@ -1632,6 +1641,7 @@ pub const JestPrettyFormat = struct {
};
if (print_children) {
var did_print_children = false;
print_children: {
switch (tag.tag) {
.String => {
@@ -1640,6 +1650,7 @@ pub const JestPrettyFormat = struct {
if (comptime enable_ansi_colors) writer.writeAll(comptime Output.prettyFmt("<r>", true));
writer.writeAll(">");
did_print_children = true;
if (children_string.len < 128) {
writer.writeString(children_string);
} else {
@@ -1654,6 +1665,7 @@ pub const JestPrettyFormat = struct {
},
.JSX => {
writer.writeAll(">\n");
did_print_children = true;
{
this.indent += 1;
@@ -1669,6 +1681,7 @@ pub const JestPrettyFormat = struct {
const length = try children.getLength(this.globalThis);
if (length == 0) break :print_children;
writer.writeAll(">\n");
did_print_children = true;
{
this.indent += 1;
@@ -1706,12 +1719,13 @@ pub const JestPrettyFormat = struct {
writer.writeAll(">");
}
return;
if (did_print_children) return;
}
}
}
}
// Self-closing tag
writer.writeAll(" />");
},
.Object => {

View File

@@ -0,0 +1,190 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
test("JSX attributes with null and other non-string values should be wrapped in braces", async () => {
using dir = tempDir("jsx-test", {
"jsx-test.js": `
const React = {
createElement: (tag, props, ...children) => {
return {
$$typeof: Symbol.for('react.element'),
type: tag,
props: { ...props, children },
key: null,
ref: null
};
}
};
const Component = () => {};
// Test various value types
const element = React.createElement(Component, {
nullProp: null,
undefinedProp: undefined,
boolTrue: true,
boolFalse: false,
number: 42,
negNumber: -3.14,
string: "hello",
emptyString: "",
zero: 0
});
console.log(element);
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), join(dir, "jsx-test.js")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Check that non-string values are wrapped in braces
expect(stdout).toContain("nullProp={null}");
expect(stdout).toContain("undefinedProp={undefined}");
expect(stdout).toContain("boolTrue={true}");
expect(stdout).toContain("boolFalse={false}");
expect(stdout).toContain("number={42}");
expect(stdout).toContain("negNumber={-3.14}");
expect(stdout).toContain("zero={0}");
// Check that string values are quoted without braces
expect(stdout).toContain('string="hello"');
expect(stdout).toContain('emptyString=""');
// Ensure invalid syntax is NOT present
expect(stdout).not.toContain("nullProp=null");
expect(stdout).not.toContain("boolTrue=true");
expect(stdout).not.toContain("number=42");
});
test("JSX elements with empty children arrays should render self-closing tags", async () => {
using dir = tempDir("jsx-empty-children", {
"jsx-empty-children.js": `
const React = {
createElement: (tag, props, ...children) => {
return {
$$typeof: Symbol.for('react.element'),
type: tag,
props: { ...props, children },
key: null,
ref: null
};
}
};
const Component = () => {};
// Element with empty children array
const element = {
$$typeof: Symbol.for('react.element'),
type: Component,
props: { foo: null, children: [] },
key: null,
ref: null
};
console.log(element);
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), join(dir, "jsx-empty-children.js")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Should have self-closing tag
expect(stdout).toContain("/>");
// Should have the attribute with braces
expect(stdout).toContain("foo={null}");
// Should NOT be truncated (was the bug)
expect(stdout).not.toMatch(/<NoName foo={null}$/m);
});
test("JSX formatting for complex nested elements", async () => {
using dir = tempDir("jsx-complex", {
"jsx-complex.js": `
const React = {
createElement: (tag, props, ...children) => {
return {
$$typeof: Symbol.for('react.element'),
type: tag,
props: { ...props, children },
key: null,
ref: null
};
}
};
const Component = () => {};
// Complex nested element
const element = React.createElement('div',
{ className: "container", id: "main", data: null },
React.createElement('span', { count: 0 }, "Hello"),
React.createElement(Component, {
flag: false,
value: undefined,
handler: () => {}
})
);
console.log(element);
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), join(dir, "jsx-complex.js"), "--console-depth=10"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Parent element attributes
expect(stdout).toContain('className="container"');
expect(stdout).toContain('id="main"');
expect(stdout).toContain("data={null}");
// Nested elements
expect(stdout).toContain("<span");
expect(stdout).toContain("count={0}");
expect(stdout).toContain("<NoName");
expect(stdout).toContain("flag={false}");
expect(stdout).toContain("value={undefined}");
expect(stdout).toContain("handler={[Function: handler]}");
});

View File

@@ -348,9 +348,9 @@ it("inspect", () => {
expect(Bun.inspect(new Map([["foo", "bar"]]))).toBe('Map(1) {\n "foo": "bar",\n}');
expect(Bun.inspect(new Set(["bar"]))).toBe('Set(1) {\n "bar",\n}');
expect(Bun.inspect(<div>foo</div>)).toBe("<div>foo</div>");
expect(Bun.inspect(<div hello>foo</div>)).toBe("<div hello=true>foo</div>");
expect(Bun.inspect(<div hello={1}>foo</div>)).toBe("<div hello=1>foo</div>");
expect(Bun.inspect(<div hello={123}>hi</div>)).toBe("<div hello=123>hi</div>");
expect(Bun.inspect(<div hello>foo</div>)).toBe("<div hello={true}>foo</div>");
expect(Bun.inspect(<div hello={1}>foo</div>)).toBe("<div hello={1}>foo</div>");
expect(Bun.inspect(<div hello={123}>hi</div>)).toBe("<div hello={123}>hi</div>");
expect(Bun.inspect(<div hello="quoted">quoted</div>)).toBe('<div hello="quoted">quoted</div>');
expect(
Bun.inspect(