From 8dd582f731fcdcf355242a8077ee512d9b233978 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Fri, 29 May 2026 22:01:49 +0100 Subject: [PATCH] fix(toml): emit TOML literals for Infinity/NaN/Date in arrays Primitive and mixed array stringification went through code paths that produced invalid TOML for non-finite numbers and Date values: - `[Infinity, -Infinity, NaN]` rendered as `[null,null,null]` because `#arrayDeclaration` used `JSON.stringify` which has no TOML equivalents for those values. - `[new Date(0)]` rendered as `["1970-01-01T00:00:00.000Z"]` for the same reason - a quoted ISO string instead of a TOML datetime literal. - Mixed arrays like `[Infinity, {}]` and `[new Date(0), {}]` ran through `#printAsInlineValue` which returned numbers verbatim (so Infinity printed as the JS identifier) and wrapped Dates in string quotes. Introduce `#printPrimitive` that produces TOML-compliant literals for Date, string, RegExp, number (including inf/-inf/nan) and boolean. `#arrayDeclaration` and `#printAsInlineValue` now route primitives through it. The behaviour for `null`/`undefined` inside arrays is left unchanged (still throws "Should never reach") since the issue author flagged it as a design call for the maintainers. Updates one existing test that encoded the old quoted-Date output inside a mixed array. The new output matches the TOML 1.0 spec for datetime literals. Fixes #7162 (partial - null handling deferred per the issue). --- toml/stringify.ts | 26 ++++++++------- toml/stringify_test.ts | 73 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/toml/stringify.ts b/toml/stringify.ts index 05a53e8e3d5e..c9ebf549a36c 100644 --- a/toml/stringify.ts +++ b/toml/stringify.ts @@ -132,24 +132,28 @@ 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)) { const str = Object.keys(value).map((key) => { return `${joinKeys([key])} = ${ // deno-lint-ignore no-explicit-any @@ -157,8 +161,7 @@ class Dumper { }).join(","); return `{${str}}`; } - - throw new Error("Should never reach"); + return this.#printPrimitive(value); } #isSimplySerializable(value: unknown): boolean { return ( @@ -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)}`; diff --git a/toml/stringify_test.ts b/toml/stringify_test.ts index 523eb45d5ec3..0d44e9071169 100644 --- a/toml/stringify_test.ts +++ b/toml/stringify_test.ts @@ -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}]] @@ -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 +`, + ); + }, +});