Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions toml/stringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,33 +132,36 @@ class Dumper {
}
return onlyPrimitive ? "ONLY_PRIMITIVE" : "ONLY_OBJECT_EXCLUDING_ARRAY";
}
#printAsInlineValue(value: unknown): string | number {
#printPrimitive(value: unknown): string {
if (value instanceof Date) {
return `"${this.#printDate(value)}"`;
return this.#printDate(value);
} else if (typeof value === "string" || value instanceof RegExp) {
return JSON.stringify(value.toString());
} else if (typeof value === "number") {
return value;
if (Number.isNaN(value)) return "nan";
if (value === Infinity) return "inf";
if (value === -Infinity) return "-inf";
return String(value);
} else if (typeof value === "boolean") {
return value.toString();
} else if (
}
throw new Error("Should never reach");
}
#printAsInlineValue(value: unknown): string {
if (
value instanceof Array
) {
const str = value.map((x) => this.#printAsInlineValue(x)).join(",");
return `[${str}]`;
} else if (typeof value === "object") {
if (!value) {
throw new Error("Should never reach");
}
} else if (value && typeof value === "object" && !(value instanceof Date)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch catches RegExp as well as plain objects, which silently drops RegExp values in mixed arrays. Verified against main:

stringify({ x: [/foo/, {}] })
// main:     x = ["/foo/",{}]   ✅
// this PR:  x = [{},{}]        ❌  RegExp lost

Reason: typeof /foo/ === "object" and a RegExp isn't a Date, so a RegExp enters this inline-table branch, Object.keys(/foo/) is [], and it serializes as {}. The old code handled RegExp before the object branch (via the string || RegExp arm), so it never reached here.

Note the asymmetry that hides it: primitive arrays go through #arrayDeclaration -> #printPrimitive, which handles RegExp fine (["/a/","/b/"]). Only the mixed path is broken, and there's no test for RegExp-in-mixed-array, so the suite stays green.

Suggested fix — exclude RegExp the same way Date is:

} else if (
  value && typeof value === "object" &&
  !(value instanceof Date) && !(value instanceof RegExp)
) {

and a regression test, e.g. assertEquals(stringify({ x: [/foo/, {}] }), 'x = ["/foo/",{}]\n').

const str = Object.keys(value).map((key) => {
return `${joinKeys([key])} = ${
// deno-lint-ignore no-explicit-any
this.#printAsInlineValue((value as any)[key])}`;
}).join(",");
return `{${str}}`;
}

throw new Error("Should never reach");
return this.#printPrimitive(value);
}
#isSimplySerializable(value: unknown): boolean {
return (
Expand All @@ -185,7 +188,8 @@ class Dumper {
return `${title} = `;
}
#arrayDeclaration(keys: string[], value: unknown[]): string {
return `${this.#declaration(keys)}${JSON.stringify(value)}`;
const items = value.map((v) => this.#printPrimitive(v)).join(",");
return `${this.#declaration(keys)}[${items}]`;
}
#strDeclaration(keys: string[], value: string): string {
return `${this.#declaration(keys)}${JSON.stringify(value)}`;
Expand Down
73 changes: 72 additions & 1 deletion toml/stringify_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ Deno.test({
const expected = `emptyArray = []
mixedArray1 = [1,{b = 2}]
mixedArray2 = [{b = 2},1]
nestedArray1 = [[{b = 1,date = "2022-05-13T00:00:00.000"}]]
nestedArray1 = [[{b = 1,date = 2022-05-13T00:00:00.000}]]
nestedArray2 = [[[{b = 1}]]]
nestedArray3 = [[],[{b = 1}]]

Expand Down Expand Up @@ -275,3 +275,74 @@ Deno.test({
assertEquals(actual, expected);
},
});

// https://github.com/denoland/std/issues/7162
Deno.test({
name:
"stringify() emits TOML float literals for Infinity/NaN in primitive arrays",
fn() {
const actual = stringify({ x: [Infinity, -Infinity, NaN] });
assertEquals(actual, "x = [inf,-inf,nan]\n");
},
});

// https://github.com/denoland/std/issues/7162
Deno.test({
name: "stringify() emits TOML datetime literal for Date in primitive arrays",
fn() {
const actual = stringify({ x: [new Date(0)] });
assertEquals(actual, "x = [1970-01-01T00:00:00.000]\n");
},
});

// https://github.com/denoland/std/issues/7162
Deno.test({
name:
"stringify() emits TOML float literals for Infinity/NaN in mixed arrays",
fn() {
const actual = stringify({ x: [Infinity, -Infinity, NaN, {}] });
assertEquals(actual, "x = [inf,-inf,nan,{}]\n");
},
});

// https://github.com/denoland/std/issues/7162
Deno.test({
name: "stringify() emits TOML datetime literal for Date in mixed arrays",
fn() {
const actual = stringify({ x: [new Date(0), {}] });
assertEquals(actual, "x = [1970-01-01T00:00:00.000,{}]\n");
},
});

// https://github.com/denoland/std/issues/7162 - regression check for the new
// #printPrimitive path that replaced JSON.stringify: finite numbers in
// primitive arrays must keep their JSON representation.
Deno.test({
name: "stringify() preserves finite numbers in primitive arrays",
fn() {
const actual = stringify({ x: [1, -1, 0, 3.14, 1e6] });
assertEquals(actual, "x = [1,-1,0,3.14,1000000]\n");
},
});

// https://github.com/denoland/std/issues/7162 - bare numeric and Date scalar
// declarations (not array elements) must be unaffected by the array fixes.
Deno.test({
name: "stringify() preserves scalar Infinity/NaN/Date declarations",
fn() {
const actual = stringify({
pi: Infinity,
bad: NaN,
neg: -Infinity,
when: new Date(0),
});
assertEquals(
actual,
`pi = inf
bad = nan
neg = -inf
when = 1970-01-01T00:00:00.000
`,
);
},
});
Loading