mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 18:43:44 +00:00
fix(formatter_json): preserve key and literal value for json-stringify (#23996)
Part of #23923 Do not transform object key, string and number values with `json-stringify`.
This commit is contained in:
1 parent
7d663d3fd0
commit
0a6b16c424
7 files changed
+63
-100
No files matched your search
@@ -44,6 +44,10 @@ pub fn write_quoted_str<'a>(f: &mut JsonFormatter<'_, 'a>, quote_byte: u8, body:
|
||||
/// 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 {
|
||||
// PERF: Use `dragonbox_ecma` directly instead of `oxc_syntax::ToJsString`
|
||||
// The latter returns `String` (heap alloc),
|
||||
// while `Buffer::format` returns `&str` borrowed from a stack buffer.
|
||||
// This is only a `==` comparison on a hot path (every numeric key), so avoid the alloc.
|
||||
s.parse::<f64>()
|
||||
.is_ok_and(|value| value.is_finite() && dragonbox_ecma::Buffer::new().format(value) == s)
|
||||
}
|
||||
|
||||
@@ -25,10 +25,6 @@ use super::{
|
||||
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> {
|
||||
@@ -42,7 +38,7 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonStringifyValue<'a, '_> {
|
||||
Expression::BooleanLiteral(lit) => {
|
||||
write!(f, if lit.value { "true" } else { "false" });
|
||||
}
|
||||
Expression::NumericLiteral(lit) => write_number_value(lit, f),
|
||||
Expression::NumericLiteral(lit) => write_number(lit, f),
|
||||
Expression::StringLiteral(lit) => write_string(lit, f),
|
||||
Expression::ArrayExpression(array) => write_array(array, f),
|
||||
Expression::ObjectExpression(object) => write_object(object, f),
|
||||
@@ -70,31 +66,14 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonStringifyValue<'a, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
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_number<'a>(lit: &NumericLiteral<'a>, f: &mut JsonFormatter<'_, 'a>) {
|
||||
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);
|
||||
}
|
||||
// `preferred_quote` already pins `json-stringify` to `"`.
|
||||
FmtJsonString { lit }.fmt(f);
|
||||
}
|
||||
|
||||
/// The always-expanded layout shared by arrays and objects: a [`block_indent`]
|
||||
@@ -165,8 +144,7 @@ fn write_object<'a>(object: &ObjectExpression<'a>, f: &mut JsonFormatter<'_, 'a>
|
||||
/// - 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"`
|
||||
/// (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),
|
||||
@@ -174,23 +152,12 @@ fn write_object_key<'a>(key: &PropertyKey<'a>, f: &mut JsonFormatter<'_, 'a>) {
|
||||
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);
|
||||
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 {
|
||||
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, text(raw));
|
||||
}
|
||||
}
|
||||
_ => write!(f, FormatInvalidJson(key.span())),
|
||||
@@ -285,8 +252,6 @@ fn json_stringify_escape(content: &str, lone_surrogates: bool) -> Cow<'_, str> {
|
||||
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
|
||||
|
||||
+4
-4
@@ -19,8 +19,8 @@ source: crates/oxc_formatter_json/tests/fixtures/mod.rs
|
||||
1,
|
||||
2,
|
||||
"3",
|
||||
10000000000,
|
||||
0.001
|
||||
1e10,
|
||||
1e-3
|
||||
]
|
||||
},
|
||||
"empty_arr": [],
|
||||
@@ -41,8 +41,8 @@ source: crates/oxc_formatter_json/tests/fixtures/mod.rs
|
||||
1,
|
||||
2,
|
||||
"3",
|
||||
10000000000,
|
||||
0.001
|
||||
1e10,
|
||||
1e-3
|
||||
]
|
||||
},
|
||||
"empty_arr": [],
|
||||
|
||||
+24
-24
@@ -15,18 +15,18 @@ source: crates/oxc_formatter_json/tests/fixtures/mod.rs
|
||||
---------------------------------------------
|
||||
[
|
||||
0.5,
|
||||
0.8675309,
|
||||
8675309,
|
||||
912559,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
-9876.54321,
|
||||
1.23456789e+34,
|
||||
2.3456789012e+76,
|
||||
123456,
|
||||
.8675309,
|
||||
8675309.,
|
||||
0xdecaf,
|
||||
1e00,
|
||||
2e+00,
|
||||
2e-00,
|
||||
1.0,
|
||||
1.00000,
|
||||
-9876.543210,
|
||||
1.234567890E+34,
|
||||
23456789012E66,
|
||||
123_456,
|
||||
1,
|
||||
-1,
|
||||
0.5,
|
||||
@@ -42,18 +42,18 @@ source: crates/oxc_formatter_json/tests/fixtures/mod.rs
|
||||
----------------------------------------------
|
||||
[
|
||||
0.5,
|
||||
0.8675309,
|
||||
8675309,
|
||||
912559,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
-9876.54321,
|
||||
1.23456789e+34,
|
||||
2.3456789012e+76,
|
||||
123456,
|
||||
.8675309,
|
||||
8675309.,
|
||||
0xdecaf,
|
||||
1e00,
|
||||
2e+00,
|
||||
2e-00,
|
||||
1.0,
|
||||
1.00000,
|
||||
-9876.543210,
|
||||
1.234567890E+34,
|
||||
23456789012E66,
|
||||
123_456,
|
||||
1,
|
||||
-1,
|
||||
0.5,
|
||||
|
||||
+16
-16
@@ -31,15 +31,15 @@ source: crates/oxc_formatter_json/tests/fixtures/mod.rs
|
||||
"undefined": "",
|
||||
"string": "",
|
||||
"0": "",
|
||||
"100": "",
|
||||
"100": "",
|
||||
1e2: "",
|
||||
1.0e+2: "",
|
||||
.1: "",
|
||||
"0.1": "",
|
||||
"0.1": "",
|
||||
"1": "",
|
||||
"1": "",
|
||||
"16": "",
|
||||
"1e+30": "",
|
||||
"123": ""
|
||||
1.0: "",
|
||||
1.00000: "",
|
||||
0x10: "",
|
||||
999999999999999999999999999999: "",
|
||||
1_2_3: ""
|
||||
}
|
||||
|
||||
----------------------------------------------
|
||||
@@ -52,15 +52,15 @@ source: crates/oxc_formatter_json/tests/fixtures/mod.rs
|
||||
"undefined": "",
|
||||
"string": "",
|
||||
"0": "",
|
||||
"100": "",
|
||||
"100": "",
|
||||
1e2: "",
|
||||
1.0e+2: "",
|
||||
.1: "",
|
||||
"0.1": "",
|
||||
"0.1": "",
|
||||
"1": "",
|
||||
"1": "",
|
||||
"16": "",
|
||||
"1e+30": "",
|
||||
"123": ""
|
||||
1.0: "",
|
||||
1.00000: "",
|
||||
0x10: "",
|
||||
999999999999999999999999999999: "",
|
||||
1_2_3: ""
|
||||
}
|
||||
|
||||
===================== End =====================
|
||||
@@ -23,7 +23,7 @@ source: crates/oxc_formatter_json/tests/fixtures/mod.rs
|
||||
"single",
|
||||
"double",
|
||||
"I can use \"double quotes\" here",
|
||||
"ģ䕧 ÿ \u0000 / /",
|
||||
"ģ䕧 \xFF \0 / \/",
|
||||
"",
|
||||
"foo",
|
||||
"\n multiple-line\n",
|
||||
@@ -37,7 +37,7 @@ source: crates/oxc_formatter_json/tests/fixtures/mod.rs
|
||||
"single",
|
||||
"double",
|
||||
"I can use \"double quotes\" here",
|
||||
"ģ䕧 ÿ \u0000 / /",
|
||||
"ģ䕧 \xFF \0 / \/",
|
||||
"",
|
||||
"foo",
|
||||
"\n multiple-line\n",
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
json-stringify compatibility: 8/14 (57.14%), 0 files skipped
|
||||
json-stringify compatibility: 14/14 (100.00%), 0 files skipped
|
||||
|
||||
# Failed
|
||||
|
||||
| Spec path | Failed or Passed | Match ratio |
|
||||
| :-------- | :--------------: | :---------: |
|
||||
| json/json/json5.json | 💥 | 82.35% |
|
||||
| json/json/json6.json | 💥 | 77.91% |
|
||||
| json/json/multi-line.json | 💥 | 87.50% |
|
||||
| json/json/pass1.json | 💥 | 81.33% |
|
||||
| json/json/propertyKey.json | 💥 | 33.33% |
|
||||
| json/json/single-line.json | 💥 | 87.50% |
|
||||
Reference in new issue
Block a user