feat(formatter_json): implement json-stringify variant (#23192)

Implement last missing JSON family variant: `json-stringify`.

Format output matches `prettier@3.8.4` for now, but we know `prettier@3.9.0` will be coming soon.

> https://github.com/prettier/prettier/blob/main/changelog_unreleased/json/18405.md

So I also implemented this and disabled under `COMPAT_PRE_18405` flag.
This commit is contained in:
leaysgur committed 2026-06-10 05:41:02 +00:00
1 parent bb0ed44541
commit 8cc82c4cdb
24 files changed
+781 -28

No files matched your search

Generated
+1
View File
@@ -2201,6 +2201,7 @@ dependencies = [
name = "oxc_formatter_json"
version = "0.54.0"
dependencies = [
"dragonbox_ecma",
"insta",
"oxc_allocator",
"oxc_ast",
+2 -1
View File
@@ -17,13 +17,14 @@ description.workspace = true
workspace = true
[dependencies]
dragonbox_ecma = { workspace = true }
oxc_allocator = { workspace = true }
oxc_ast = { workspace = true }
oxc_diagnostics = { workspace = true }
oxc_formatter_core = { workspace = true }
oxc_parser = { workspace = true }
oxc_span = { workspace = true }
oxc_syntax = { workspace = true, features = ["to_js_string"] }
oxc_syntax = { workspace = true }
[dev-dependencies]
insta = { workspace = true }
+8 -4
View File
@@ -2,7 +2,7 @@ use oxc_allocator::Allocator;
use oxc_ast::ast::Expression;
use oxc_diagnostics::OxcDiagnostic;
use oxc_formatter_core::{
Buffer, Document, Format, FormatState, Formatted, VecBuffer,
Buffer, Document, Format, FormatContext, FormatState, Formatted, VecBuffer,
builders::{hard_line_break, text},
write,
};
@@ -12,9 +12,9 @@ use oxc_syntax::identifier::ZWNBSP;
use crate::{
comments::write_trailing_inside_comments,
context::JsonFormatContext,
options::JsonFormatOptions,
options::{JsonFormatOptions, JsonVariant},
parse::parse_json,
print::{FmtJsonValue, JsonFormatter},
print::{FmtJsonStringifyValue, FmtJsonValue, JsonFormatter},
};
/// Parse `source_text` as JSON and build its formatter IR.
@@ -71,7 +71,11 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FormatJsonRoot<'a, '_> {
}
let trailing_anchor = if let Some(expression) = self.expression {
FmtJsonValue { expression }.fmt(f);
if f.context().options().variant == JsonVariant::JsonStringify {
FmtJsonStringifyValue { expression }.fmt(f);
} else {
FmtJsonValue { expression }.fmt(f);
}
expression.span().end
} else {
// Comments-only source: emit pending comments from the start of the source
+2 -4
View File
@@ -56,10 +56,8 @@ impl JsonFormatOptions {
/// per the active variant and [`Self::trailing_commas`].
///
/// `json` and `json-stringify` never emit one, but Prettier achieves this differently:
/// - `json`: the option is force-normalized to `trailingComma: "none"`.
/// - `json-stringify`: a separate `estree-json` printer is used
/// - Always hard-breaks entries and never emits a trailing comma (the option is irrelevant)
/// - So full `json-stringify` parity will also need always-expand + no concise arrays, not just this flag
/// - `json`: the option is force-normalized to `trailingComma: "none"`
/// - `json-stringify`: a separate `estree-json` printer is used and never emits a trailing comma
///
/// `jsonc` and `json5` follow the user option (`Always` emits, `Never` does not).
/// Both go through Prettier's shared `estree` printer with `shouldPrintTrailingComma` fixed at the `es5` level,
@@ -12,6 +12,9 @@ use crate::{
pub mod array;
pub mod literal;
pub mod object;
mod stringify;
pub use stringify::FmtJsonStringifyValue;
pub type JsonFormatter<'buf, 'a> = Formatter<'buf, 'a, JsonFormatContext<'a>>;
@@ -42,6 +45,20 @@ pub fn write_quoted_str<'a>(f: &mut JsonFormatter<'_, 'a>, quote_byte: u8, body:
write!(f, [text(quote), text(body), text(quote)]);
}
/// Emulates JS `String(Number(s)) === s`: `true` when `s` parses as a finite `f64`
/// whose ECMAScript string form is byte-identical to `s`.
/// Both the `json`/`jsonc` and `json-stringify` numeric-key quoting rules build on this.
///
/// Rust's `f64` parsing and JS `Number()` differ on some inputs
/// (hex / binary / octal prefixes, numeric separators),
/// but every such input fails the round-trip on both sides.
/// JS stringifies the parsed value into a different shape (`"16"` for `0x10`, `"NaN"` for `1_2`),
/// Rust simply fails to parse, so `Err` mapping to `false` matches.
pub fn number_string_round_trips(s: &str) -> bool {
s.parse::<f64>()
.is_ok_and(|value| value.is_finite() && dragonbox_ecma::Buffer::new().format(value) == s)
}
/// Wraps a re-entrant JSON closure in a [`FormatWith`]. The closure's context is
/// pinned to [`JsonFormatContext`] so call sites don't have to annotate it.
#[inline]
@@ -8,7 +8,7 @@ use oxc_formatter_core::{
write,
};
use oxc_span::GetSpan;
use oxc_syntax::{identifier::is_identifier_name_patched, number::ToJsString};
use oxc_syntax::identifier::is_identifier_name_patched;
use crate::{
comments::{
@@ -22,7 +22,7 @@ use crate::{
use super::{
FmtJsonValue, FormatInvalidJson, JsonFormatter, arena_cow_str, format_with,
literal::FmtJsonString, write_quoted_str,
literal::FmtJsonString, number_string_round_trips, write_quoted_str,
};
pub struct FmtJsonObject<'a, 'b> {
@@ -277,9 +277,5 @@ fn normalized_numeric_key<'a>(lit: &NumericLiteral<'a>, f: &JsonFormatter<'_, 'a
/// This keeps `0`/`0.1` quoted while leaving `1e2`/`1.0`/`0xdecaf`/ `999999999999999999999999999999`/`0.000...001`
/// (which lose precision or change shape under JS `String(Number)`) unquoted.
fn should_quote_numeric_key(printed: &str) -> bool {
if !is_simple_number(printed) {
return false;
}
let Ok(v) = printed.parse::<f64>() else { return false };
v.to_js_string() == printed
is_simple_number(printed) && number_string_round_trips(printed)
}
@@ -0,0 +1,324 @@
//! Dedicated printer for the `json-stringify` variant.
//!
//! Comments never reach this printer:
//! the `json-stringify` variant rejects them at parse time (see `parse::validate_comments_for_variant`),
//! which is also why none of the comment / suppression machinery appears here.
use std::borrow::Cow;
use oxc_ast::ast::{
ArrayExpression, ArrayExpressionElement, Expression, NumericLiteral, ObjectExpression,
ObjectPropertyKind, PropertyKey, StringLiteral, TemplateLiteral,
};
use oxc_formatter_core::{
Buffer, Format,
builders::{block_indent, hard_line_break, space, text},
write,
};
use oxc_span::GetSpan;
use oxc_syntax::operator::UnaryOperator;
use crate::context::JsonFormatContext;
use super::{
FormatInvalidJson, JsonFormatter, arena_cow_str, format_with, literal::FmtJsonString,
number_string_round_trips, write_quoted_str,
};
// NOTE: This is a temporary compatibility flag for Prettier's pre-#18405 behavior,
// which should be removed after updating `prettier@3.9.0`.
const COMPAT_PRE_18405: bool = true;
/// Top-level wrapper around an [`Expression`] for the `json-stringify` variant.
/// The counterpart of [`super::FmtJsonValue`]; recursion stays within this type.
pub struct FmtJsonStringifyValue<'a, 'b> {
pub expression: &'b Expression<'a>,
}
impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonStringifyValue<'a, '_> {
fn fmt(&self, f: &mut JsonFormatter<'_, 'a>) {
match self.expression {
Expression::NullLiteral(_) => write!(f, "null"),
Expression::BooleanLiteral(lit) => {
write!(f, if lit.value { "true" } else { "false" });
}
Expression::NumericLiteral(lit) => write_number_value(lit, f),
Expression::StringLiteral(lit) => write_string(lit, f),
Expression::ArrayExpression(array) => write_array(array, f),
Expression::ObjectExpression(object) => write_object(object, f),
Expression::UnaryExpression(unary) => match unary.operator {
// `JSON.parse` rejects a leading `+`, so Prettier drops it.
UnaryOperator::UnaryPlus => {
Self { expression: &unary.argument }.fmt(f);
}
UnaryOperator::UnaryNegation => {
write!(f, "-");
Self { expression: &unary.argument }.fmt(f);
}
_ => write!(f, FormatInvalidJson(unary.span)),
},
// JSON5 `Infinity` / `NaN`, JSON6 `undefined`; anything else is invalid
// (Prettier rejects other identifiers at parse time, we report at format time).
Expression::Identifier(ident)
if matches!(ident.name.as_str(), "Infinity" | "NaN" | "undefined") =>
{
write!(f, text(ident.name.as_str()));
}
Expression::TemplateLiteral(template) => write_template(template, f),
_ => write!(f, FormatInvalidJson(self.expression.span())),
}
}
}
fn write_number_value<'a>(lit: &NumericLiteral<'a>, f: &mut JsonFormatter<'_, 'a>) {
if COMPAT_PRE_18405 {
// The parser can only produce non-negative values; overflow yields +Infinity.
if lit.value.is_finite() {
// Stack-format then copy once into the arena (same as `oxc_codegen`),
// avoiding `ToJsString`'s intermediate heap `String`.
let mut buffer = dragonbox_ecma::Buffer::new();
write!(f, text(f.allocator().alloc_str(buffer.format(lit.value))));
} else {
write!(f, "null");
}
} else {
let raw = lit.raw.as_ref().unwrap_or_else(|| unreachable!("parser always sets `raw`"));
write!(f, text(raw.as_str()));
}
}
fn write_string<'a>(lit: &StringLiteral<'a>, f: &mut JsonFormatter<'_, 'a>) {
if COMPAT_PRE_18405 {
let body = json_stringify_escape(lit.value.as_str(), lit.lone_surrogates);
write_quoted_str(f, b'"', arena_cow_str(body, f));
} else {
// `preferred_quote` already pins `json-stringify` to `"`.
FmtJsonString { lit }.fmt(f);
}
}
/// The always-expanded layout shared by arrays and objects: a [`block_indent`]
/// whose body is `len` entries joined by `,` + hard line break.
fn write_hard_broken_entries<'a>(
f: &mut JsonFormatter<'_, 'a>,
len: usize,
write_entry: impl Fn(usize, &mut JsonFormatter<'_, 'a>),
) {
let body = format_with(|f| {
for i in 0..len {
if i > 0 {
write!(f, [",", hard_line_break()]);
}
write_entry(i, f);
}
});
write!(f, block_indent(&body));
}
/// `[` + one element per line + `]`.
/// Holes print as `null`,
/// this matches the Babel AST's `null` elements, which Prettier's printer turns into the string `"null"`).
fn write_array<'a>(array: &ArrayExpression<'a>, f: &mut JsonFormatter<'_, 'a>) {
write!(f, "[");
if array.elements.is_empty() {
write!(f, "]");
return;
}
write_hard_broken_entries(f, array.elements.len(), |i, f| {
let element = &array.elements[i];
if matches!(element, ArrayExpressionElement::Elision(_)) {
write!(f, "null");
} else if let Some(expression) = element.as_expression() {
FmtJsonStringifyValue { expression }.fmt(f);
} else {
// The only remaining variant (after `Elision` and `Expression`) is `SpreadElement`.
write!(f, FormatInvalidJson(element.span()));
}
});
write!(f, "]");
}
/// `{` + one `key: value` per line + `}`.
fn write_object<'a>(object: &ObjectExpression<'a>, f: &mut JsonFormatter<'_, 'a>) {
write!(f, "{");
if object.properties.is_empty() {
write!(f, "}");
return;
}
write_hard_broken_entries(f, object.properties.len(), |i, f| match &object.properties[i] {
ObjectPropertyKind::ObjectProperty(prop) => {
write_object_key(&prop.key, f);
write!(f, [":", space()]);
FmtJsonStringifyValue { expression: &prop.value }.fmt(f);
}
ObjectPropertyKind::SpreadProperty(spread) => {
write!(f, FormatInvalidJson(spread.span));
}
});
write!(f, "}");
}
/// Object-key printing per Prettier's `estree-json`:
/// - String key: same as a string value
/// - Identifier key (`a:`, `undefined:`): always double-quoted
/// (identifier names cannot contain characters `JSON.stringify` would escape)
/// - Numeric key: see the module table for the mode split; in compat mode `String()`
/// (unlike value position's `JSON.stringify()`) has no `null` fallback,
/// so non-finite quotes as `"Infinity"`
fn write_object_key<'a>(key: &PropertyKey<'a>, f: &mut JsonFormatter<'_, 'a>) {
match key {
PropertyKey::StringLiteral(lit) => write_string(lit, f),
PropertyKey::StaticIdentifier(ident) => {
write_quoted_str(f, b'"', ident.name.as_str());
}
PropertyKey::NumericLiteral(lit) => {
if COMPAT_PRE_18405 {
let printed = if lit.value.is_finite() {
let mut buffer = dragonbox_ecma::Buffer::new();
f.allocator().alloc_str(buffer.format(lit.value))
} else {
"Infinity"
};
write_quoted_str(f, b'"', printed);
} else {
let raw =
lit.raw.as_ref().unwrap_or_else(|| unreachable!("parser always sets `raw`"));
let raw = raw.as_str();
if number_string_round_trips(raw) {
write_quoted_str(f, b'"', raw);
} else {
write!(f, text(raw));
}
}
}
_ => write!(f, FormatInvalidJson(key.span())),
}
}
/// A template literal is JSON-printable only without substitutions:
/// `` `foo` `` → `"foo"` via `JSON.stringify(cooked)`
fn write_template<'a>(template: &TemplateLiteral<'a>, f: &mut JsonFormatter<'_, 'a>) {
if !template.expressions.is_empty() || template.quasis.len() != 1 {
write!(f, FormatInvalidJson(template.span));
return;
}
let quasi = &template.quasis[0];
// `cooked` is `None` for invalid escape sequences
// (only possible in tagged templates, but stay defensive here).
let Some(cooked) = &quasi.value.cooked else {
write!(f, FormatInvalidJson(template.span));
return;
};
let body = json_stringify_escape(cooked.as_str(), quasi.lone_surrogates);
write_quoted_str(f, b'"', arena_cow_str(body, f));
}
/// Escapes `content` the way `JSON.stringify` does for a string body
/// (the quotes themselves are not included): `"` and `\` get a backslash,
/// control characters use their short escapes (`\b\f\n\r\t`) or `\uXXXX`.
///
/// When `lone_surrogates` is set, `content` encodes each lone surrogate as
/// `\u{FFFD}XXXX` (and a literal U+FFFD as `\u{FFFD}fffd`).
/// The same scheme `oxc_codegen` decodes, `JSON.stringify` prints lone surrogates as `\uXXXX`.
fn json_stringify_escape(content: &str, lone_surrogates: bool) -> Cow<'_, str> {
const HEX: &[u8; 16] = b"0123456789abcdef";
if !lone_surrogates && !content.bytes().any(|b| matches!(b, b'"' | b'\\') || b < 0x20) {
return Cow::Borrowed(content);
}
let mut out = String::with_capacity(content.len() + 8);
// Start of the pending run of as-is characters, flushed in one `push_str`
// before each escape (mirrors `oxc_formatter_core::spec::normalize_string`).
let mut copy_start = 0;
let mut chars = content.char_indices();
while let Some((i, c)) = chars.next() {
let short_escape = match c {
'"' => Some("\\\""),
'\\' => Some("\\\\"),
'\u{8}' => Some("\\b"),
'\u{c}' => Some("\\f"),
'\n' => Some("\\n"),
'\r' => Some("\\r"),
'\t' => Some("\\t"),
_ => None,
};
if let Some(escaped) = short_escape {
out.push_str(&content[copy_start..i]);
out.push_str(escaped);
copy_start = i + 1; // every short-escaped char is a single byte
} else if (c as u32) < 0x20 {
out.push_str(&content[copy_start..i]);
// Always `\u00XX` — the guard caps the code point below 0x20.
let code = c as u32;
out.push_str("\\u00");
out.push(HEX[(code >> 4) as usize] as char);
out.push(HEX[(code & 0xF) as usize] as char);
copy_start = i + 1;
} else if c == '\u{FFFD}' && lone_surrogates {
out.push_str(&content[copy_start..i]);
// 4 lowercase-hex ASCII chars always follow the 3-byte escape marker;
// `get` only guards against malformed input the parser never produces,
// falling back (like the `fffd` self-escape) to a literal U+FFFD.
let hex_start = i + 3;
match content.get(hex_start..hex_start + 4) {
Some(hex) if hex != "fffd" => {
out.push_str("\\u");
out.push_str(hex);
}
_ => out.push('\u{FFFD}'),
}
copy_start = (hex_start + 4).min(content.len());
// Skip the 4 (single-byte) hex chars.
for _ in 0..4 {
chars.next();
}
}
}
out.push_str(&content[copy_start..]);
Cow::Owned(out)
}
#[cfg(test)]
mod tests {
use super::{json_stringify_escape, number_string_round_trips};
/// Covers the prettier@3.9 behavior even while `COMPAT_PRE_18405` keeps it inactive,
/// so the path doesn't rot before the switch is flipped.
#[test]
fn key_round_trip() {
// Quoted: survives `String(Number(raw))` unchanged
for raw in ["0", "1", "0.1", "100", "1e+30", "1e-30"] {
assert!(number_string_round_trips(raw), "{raw}");
}
// Unquoted: JS would print a different shape (or NaN)
for raw in [
"1.0",
"1.00000",
".1",
"1e2",
"1.0e+2",
"0x10",
"1_2_3",
"999999999999999999999999999999",
"1e999",
] {
assert!(!number_string_round_trips(raw), "{raw}");
}
}
#[test]
fn stringify_escape() {
assert_eq!(json_stringify_escape("plain", false), "plain");
assert_eq!(json_stringify_escape("a\"b\\c", false), "a\\\"b\\\\c");
assert_eq!(json_stringify_escape("\u{8}\u{c}\n\r\t", false), "\\b\\f\\n\\r\\t");
assert_eq!(json_stringify_escape("\0\u{1}\u{1f}", false), "\\u0000\\u0001\\u001f");
// U+2028 / U+2029 are NOT escaped by `JSON.stringify`
assert_eq!(json_stringify_escape("\u{2028}\u{2029}", false), "\u{2028}\u{2029}");
// Lone surrogate `\u{FFFD}XXXX` encoding; `\u{FFFD}fffd` is a literal U+FFFD
assert_eq!(json_stringify_escape("a\u{FFFD}d800b", true), "a\\ud800b");
assert_eq!(json_stringify_escape("a\u{FFFD}fffdb", true), "a\u{FFFD}b");
}
}
@@ -0,0 +1 @@
{"key1":[true,false,null],"key2":{"key3":[1,2,"3",1e10,1e-3]},"empty_arr":[],"empty_obj":{}}
@@ -0,0 +1,52 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
{"key1":[true,false,null],"key2":{"key3":[1,2,"3",1e10,1e-3]},"empty_arr":[],"empty_obj":{}}
==================== Output ====================
---------------------------------------------
{ printWidth: 80, variant: "json-stringify" }
---------------------------------------------
{
"key1": [
true,
false,
null
],
"key2": {
"key3": [
1,
2,
"3",
10000000000,
0.001
]
},
"empty_arr": [],
"empty_obj": {}
}
----------------------------------------------
{ printWidth: 100, variant: "json-stringify" }
----------------------------------------------
{
"key1": [
true,
false,
null
],
"key2": {
"key3": [
1,
2,
"3",
10000000000,
0.001
]
},
"empty_arr": [],
"empty_obj": {}
}
===================== End =====================
@@ -0,0 +1,10 @@
{
"a": 1,
"b": [1, 2,
3],
"c": 3
}
@@ -0,0 +1,43 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
{
"a": 1,
"b": [1, 2,
3],
"c": 3
}
==================== Output ====================
---------------------------------------------
{ printWidth: 80, variant: "json-stringify" }
---------------------------------------------
{
"a": 1,
"b": [
1,
2,
3
],
"c": 3
}
----------------------------------------------
{ printWidth: 100, variant: "json-stringify" }
----------------------------------------------
{
"a": 1,
"b": [
1,
2,
3
],
"c": 3
}
===================== End =====================
@@ -0,0 +1,6 @@
[
[,],
[1, , 2,,,,],
[1, , 2],
[1, , 2,]
]
@@ -0,0 +1,67 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
[
[,],
[1, , 2,,,,],
[1, , 2],
[1, , 2,]
]
==================== Output ====================
---------------------------------------------
{ printWidth: 80, variant: "json-stringify" }
---------------------------------------------
[
[
null
],
[
1,
null,
2,
null,
null,
null
],
[
1,
null,
2
],
[
1,
null,
2
]
]
----------------------------------------------
{ printWidth: 100, variant: "json-stringify" }
----------------------------------------------
[
[
null
],
[
1,
null,
2,
null,
null,
null
],
[
1,
null,
2
],
[
1,
null,
2
]
]
===================== End =====================
@@ -0,0 +1,6 @@
[
0.5, .8675309, 8675309., 0xdecaf, 1e00, 2e+00, 2e-00, 1.0, 1.00000,
-9876.543210, 1.234567890E+34, 23456789012E66, 123_456,
+1, -1, +0.5,
Infinity, -Infinity, NaN, -NaN, undefined
]
@@ -0,0 +1,67 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
[
0.5, .8675309, 8675309., 0xdecaf, 1e00, 2e+00, 2e-00, 1.0, 1.00000,
-9876.543210, 1.234567890E+34, 23456789012E66, 123_456,
+1, -1, +0.5,
Infinity, -Infinity, NaN, -NaN, undefined
]
==================== Output ====================
---------------------------------------------
{ printWidth: 80, variant: "json-stringify" }
---------------------------------------------
[
0.5,
0.8675309,
8675309,
912559,
1,
2,
2,
1,
1,
-9876.54321,
1.23456789e+34,
2.3456789012e+76,
123456,
1,
-1,
0.5,
Infinity,
-Infinity,
NaN,
-NaN,
undefined
]
----------------------------------------------
{ printWidth: 100, variant: "json-stringify" }
----------------------------------------------
[
0.5,
0.8675309,
8675309,
912559,
1,
2,
2,
1,
1,
-9876.54321,
1.23456789e+34,
2.3456789012e+76,
123456,
1,
-1,
0.5,
Infinity,
-Infinity,
NaN,
-NaN,
undefined
]
===================== End =====================
@@ -0,0 +1 @@
[{ "variant": "json-stringify" }]
@@ -0,0 +1,17 @@
{
a: '',
null: '',
true: '',
undefined: '',
"string": "",
0: '',
1e2: '',
1.0e+2: '',
.1: '',
0.1: '',
1.0: '',
1.00000: '',
0x10: '',
999999999999999999999999999999: '',
1_2_3: ''
}
@@ -0,0 +1,66 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
{
a: '',
null: '',
true: '',
undefined: '',
"string": "",
0: '',
1e2: '',
1.0e+2: '',
.1: '',
0.1: '',
1.0: '',
1.00000: '',
0x10: '',
999999999999999999999999999999: '',
1_2_3: ''
}
==================== Output ====================
---------------------------------------------
{ printWidth: 80, variant: "json-stringify" }
---------------------------------------------
{
"a": "",
"null": "",
"true": "",
"undefined": "",
"string": "",
"0": "",
"100": "",
"100": "",
"0.1": "",
"0.1": "",
"1": "",
"1": "",
"16": "",
"1e+30": "",
"123": ""
}
----------------------------------------------
{ printWidth: 100, variant: "json-stringify" }
----------------------------------------------
{
"a": "",
"null": "",
"true": "",
"undefined": "",
"string": "",
"0": "",
"100": "",
"100": "",
"0.1": "",
"0.1": "",
"1": "",
"1": "",
"16": "",
"1e+30": "",
"123": ""
}
===================== End =====================
@@ -0,0 +1,12 @@
[
'single',
"double",
'I can use "double quotes" here',
"ģ䕧 \xFF \0 / \/",
``,
`foo`,
`
multiple-line
`,
`\u{1F409}\`'"\${}`
]
@@ -0,0 +1,47 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
[
'single',
"double",
'I can use "double quotes" here',
"ģ䕧 \xFF \0 / \/",
``,
`foo`,
`
multiple-line
`,
`\u{1F409}\`'"\${}`
]
==================== Output ====================
---------------------------------------------
{ printWidth: 80, variant: "json-stringify" }
---------------------------------------------
[
"single",
"double",
"I can use \"double quotes\" here",
"ģ䕧 ÿ \u0000 / /",
"",
"foo",
"\n multiple-line\n",
"🐉`'\"${}"
]
----------------------------------------------
{ printWidth: 100, variant: "json-stringify" }
----------------------------------------------
[
"single",
"double",
"I can use \"double quotes\" here",
"ģ䕧 ÿ \u0000 / /",
"",
"foo",
"\n multiple-line\n",
"🐉`'\"${}"
]
===================== End =====================
@@ -0,0 +1,6 @@
json-stringify compatibility: 14/14 (100.00%)
# Failed
| Spec path | Failed or Passed | Match ratio |
| :-------- | :--------------: | :---------: |
+1
View File
@@ -20,6 +20,7 @@ fn main() {
TestLanguage::Json,
TestLanguage::Jsonc,
TestLanguage::Json5,
TestLanguage::JsonStringify,
] {
TestRunner::new(TestRunnerOptions { language, debug, filter: filter.clone() }).run();
}
+5 -2
View File
@@ -15,6 +15,7 @@ pub enum TestLanguage {
Json,
Jsonc,
Json5,
JsonStringify,
}
impl TestLanguage {
@@ -25,6 +26,7 @@ impl TestLanguage {
Self::Json => "json",
Self::Jsonc => "jsonc",
Self::Json5 => "json5",
Self::JsonStringify => "json-stringify",
}
}
@@ -35,8 +37,7 @@ impl TestLanguage {
// There is no `tsx` directory, just check it works with TS
// `SourceType`.`variant` is handled by spec file extension
Self::Ts => ["typescript", "jsx"].iter().map(|dir| base.join(dir)).collect::<Vec<_>>(),
// For the JSON family (`Json`/`Jsonc`/`Json5`)
// the `json/` and `with-comment/` dirs are shared:
// For the JSON family (`Json`/`Jsonc`/`Json5`), the `json/` and `with-comment/` dirs are shared:
// each `format.test.js` call lists its own parser,
// so `spec.rs` keeps only the calls matching the active language.
//
@@ -54,6 +55,8 @@ impl TestLanguage {
base.join("json").join("with-comment"),
base.join("json").join("json5-as-json-with-trailing-commas"),
],
// `json-stringify` runs only on the shared `json/` dir
Self::JsonStringify => vec![base.join("json").join("json")],
}
}
}
+17 -10
View File
@@ -134,14 +134,19 @@ impl VisitMut<'_> for SpecParser {
return;
}
// NOTE: The `json` / `jsonc` / `json5` languages each accept only their own parser's calls.
// NOTE: The JSON-family languages each accept only their own parser's calls
// (the parser name is exactly `TestLanguage::as_str()`).
// A single `format.test.js` may list several parsers (e.g. `with-comment/`),
// so we filter per-language; `json-stringify` stays out of scope.
match self.language {
TestLanguage::Json if !parsers.iter().any(|p| p == "json") => return,
TestLanguage::Jsonc if !parsers.iter().any(|p| p == "jsonc") => return,
TestLanguage::Json5 if !parsers.iter().any(|p| p == "json5") => return,
_ => {}
// so we filter per-language.
let is_json_family = matches!(
self.language,
TestLanguage::Json
| TestLanguage::Jsonc
| TestLanguage::Json5
| TestLanguage::JsonStringify
);
if is_json_family && !parsers.iter().any(|p| p == self.language.as_str()) {
return;
}
let mut js_options = JsFormatOptions {
@@ -154,6 +159,7 @@ impl VisitMut<'_> for SpecParser {
variant: match self.language {
TestLanguage::Jsonc => JsonVariant::Jsonc,
TestLanguage::Json5 => JsonVariant::Json5,
TestLanguage::JsonStringify => JsonVariant::JsonStringify,
_ => JsonVariant::Json,
},
..Default::default()
@@ -310,9 +316,10 @@ impl VisitMut<'_> for SpecParser {
snapshot_options.sort_by(|a, b| a.0.cmp(&b.0));
let options = match self.language {
TestLanguage::Json | TestLanguage::Jsonc | TestLanguage::Json5 => {
SpecOptions::Json(json_options)
}
TestLanguage::Json
| TestLanguage::Jsonc
| TestLanguage::Json5
| TestLanguage::JsonStringify => SpecOptions::Json(json_options),
TestLanguage::Js | TestLanguage::Ts => SpecOptions::Js(Box::new(js_options)),
};
self.calls.push((options, snapshot_options));