Skip to content

fix(toml): emit TOML literals for Infinity/NaN/Date in arrays#7163

Open
spokodev wants to merge 2 commits into
denoland:mainfrom
spokodev:fix/toml-stringify-primitive-and-datetime-literals
Open

fix(toml): emit TOML literals for Infinity/NaN/Date in arrays#7163
spokodev wants to merge 2 commits into
denoland:mainfrom
spokodev:fix/toml-stringify-primitive-and-datetime-literals

Conversation

@spokodev

Copy link
Copy Markdown

Primitive and mixed array stringification went through code paths that produced invalid TOML for non-finite numbers and Date values. The issue author flagged six distinct cases in #7162; this PR addresses the four with a deterministic spec-aligned fix and leaves the two null-related ones for maintainer guidance, since they are a design call.

What changed

#arrayDeclaration (primitive arrays) previously routed values through JSON.stringify, which has no representation for Infinity, -Infinity, NaN, or TOML datetime literals. #printAsInlineValue (mixed arrays) returned numbers verbatim (so Infinity printed as the JS identifier) and wrapped Dates in string quotes.

A new private helper #printPrimitive produces TOML-compliant literals for Date, string, RegExp, number (including inf/-inf/nan) and boolean. Both array paths now route primitives through it.

Behaviour for null/undefined inside arrays is unchanged - the code still throws Should never reach. The issue author flagged this as a design call (TOML has no null), so it stays as-is until maintainers decide between a clearer error message, silent skip, or another shape.

Before/after

// primitive array
stringify({x: [Infinity, -Infinity, NaN]})
// before: x = [null,null,null]
// after:  x = [inf,-inf,nan]

stringify({x: [new Date(0)]})
// before: x = [\"1970-01-01T00:00:00.000Z\"]
// after:  x = [1970-01-01T00:00:00.000]

// mixed array
stringify({x: [Infinity, -Infinity, NaN, {}]})
// before: x = [Infinity,-Infinity,NaN,{}]   (invalid TOML)
// after:  x = [inf,-inf,nan,{}]

stringify({x: [new Date(0), {}]})
// before: x = [\"1970-01-01T00:00:00.000\",{}]
// after:  x = [1970-01-01T00:00:00.000,{}]

Tests

Six new cases in toml/stringify_test.ts:

  • positive: Infinity/-Infinity/NaN in primitive arrays render as inf/-inf/nan
  • positive: Date in primitive arrays renders as a bare TOML datetime literal
  • positive: Infinity/-Infinity/NaN in mixed arrays
  • positive: Date in mixed arrays
  • regression-check: finite numbers in primitive arrays keep their JSON-style representation (covers the new #printPrimitive path that replaced JSON.stringify)
  • regression-check: scalar Infinity/NaN/Date declarations (not array elements) are unaffected

One existing test (stringify() handles mixed array) had a date inside a mixed inline-table that was previously emitted with quotes. The expected output is updated to the spec-aligned bare datetime literal.

`deno test --allow-all toml/` -> 689 passed, 0 failed, 111 ignored.
`deno fmt` and `deno lint` clean on the changed files.

Fixes #7162 (partial - null handling left for maintainer guidance).

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 denoland#7162 (partial - null handling deferred per the issue).
@CLAassistant

CLAassistant commented May 29, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added the toml label May 29, 2026
@codecov

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.84%. Comparing base (cdf74a8) to head (3c68f02).
⚠️ Report is 18 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7163      +/-   ##
==========================================
+ Coverage   94.57%   94.84%   +0.26%     
==========================================
  Files         636      617      -19     
  Lines       52142    51682     -460     
  Branches     9401     9358      -43     
==========================================
- Hits        49315    49016     -299     
+ Misses       2249     2121     -128     
+ Partials      578      545      -33     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@spokodev spokodev marked this pull request as ready for review June 18, 2026 16:56
@spokodev

Copy link
Copy Markdown
Author

recheck

@bartlomieju bartlomieju left a comment

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.

Thanks for this — really thorough PR, and the write-up is excellent (clear before/after, honest about the deferred null design call, and well-scoped). I verified the positive fixes locally and they all work: [Infinity,-Infinity,NaN] -> [inf,-inf,nan], dates emit unquoted and reparse as Date, finite numbers are preserved, and deno test -A toml/ is 689/0 with fmt+lint clean.

There's one regression introduced by the refactor that needs a fix before this can land — RegExp values inside mixed arrays get dropped. Details inline. Once that's handled (plus a small regression test) this is in good shape.

Heads-up: #7171 is an open PR fixing the same issue (#7162) with the same inf/-inf/nan + unquoted-Date approach — worth a look so the two efforts don't collide.

Comment thread toml/stringify.ts Outdated
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').

The inline-table branch in #printAsInlineValue matched any non-Date object,
so a RegExp in a mixed array was serialized as an empty {} and dropped.
Exclude RegExp the same way Date is, so it falls through to the primitive
path. Adds a regression test.
@spokodev

Copy link
Copy Markdown
Author

Thanks for the careful review and the precise repro. Fixed: excluded RegExp from the inline-table branch the same way Date is, so a RegExp in a mixed array now serializes through the primitive path ({ x: [/foo/, {}] } becomes x = ["/foo/",{}]). Added a regression test. deno test -A toml/ is 690/0, fmt and lint clean.

On #7171: thanks for the heads-up. This PR is broader (it also covers -Infinity, NaN, and the deferred null design), so I will leave the call to you on which to land; I can rebase around the other if you prefer it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@std/toml: Inline array stringification mishandles edge cases

3 participants