mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
1 parent
bb0ed44541
commit
8cc82c4cdb
24 files changed
+781
-28
No files matched your search
Generated
+1
@@ -2201,6 +2201,7 @@ dependencies = [
|
||||
name = "oxc_formatter_json"
|
||||
version = "0.54.0"
|
||||
dependencies = [
|
||||
"dragonbox_ecma",
|
||||
"insta",
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":{}}
|
||||
+52
@@ -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
|
||||
}
|
||||
+43
@@ -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: ''
|
||||
}
|
||||
+66
@@ -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 |
|
||||
| :-------- | :--------------: | :---------: |
|
||||
@@ -20,6 +20,7 @@ fn main() {
|
||||
TestLanguage::Json,
|
||||
TestLanguage::Jsonc,
|
||||
TestLanguage::Json5,
|
||||
TestLanguage::JsonStringify,
|
||||
] {
|
||||
TestRunner::new(TestRunnerOptions { language, debug, filter: filter.clone() }).run();
|
||||
}
|
||||
|
||||
@@ -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")],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user