feat(formatter_json): implement jsonc variant (#22912)

Implement `oxc_formatter_json`'s `Jsonc` variant.

The difference between `json` and `jsonc`:

- `jsonc` allows comment only input, `json` does not
- `jsonc` refers `trailingComma` option, `json` does not
This commit is contained in:
leaysgur committed 2026-06-04 06:45:21 +00:00
1 parent beb46d31fa
commit 27a6db879b
33 files changed
+723 -100

No files matched your search

+4 -56
View File
@@ -1,11 +1,10 @@
use std::{fmt, str::FromStr};
use oxc_formatter_core::{BracketSpacing, Expand, IndentStyle, IndentWidth, LineEnding, LineWidth};
/// JS-facing alias for the language-agnostic [`oxc_formatter_core::util::Quote`].
/// Kept as `QuoteStyle` so existing public API (`JsFormatOptions::quote_style`)
/// continues to compile.
pub use oxc_formatter_core::util::Quote as QuoteStyle;
// Re-exported so the crate root's `pub use crate::options::*` keeps them on the public API.
// `QuoteStyle` is the JS-facing alias for the language-agnostic [`oxc_formatter_core::util::Quote`]
// (kept so `JsFormatOptions::quote_style` continues to compile).
pub use oxc_formatter_core::{TrailingCommas, util::Quote as QuoteStyle};
use crate::{
formatter::{
@@ -501,57 +500,6 @@ impl<'a> Format<'a, JsFormatContext<'a>> for FormatTrailingCommas {
}
}
/// Print trailing commas wherever possible in multi-line comma-separated syntactic structures.
#[derive(Clone, Copy, Default, Debug, Eq, Hash, PartialEq)]
pub enum TrailingCommas {
/// Trailing commas wherever possible (including function parameters and calls).
#[default]
All,
/// Trailing commas where valid in ES5 (objects, arrays, etc.). No trailing commas in type parameters in TypeScript.
Es5,
/// No trailing commas.
None,
}
impl TrailingCommas {
pub const fn is_es5(self) -> bool {
matches!(self, TrailingCommas::Es5)
}
pub const fn is_all(self) -> bool {
matches!(self, TrailingCommas::All)
}
pub const fn is_none(self) -> bool {
matches!(self, TrailingCommas::None)
}
}
impl FromStr for TrailingCommas {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"es5" => Ok(Self::Es5),
"all" => Ok(Self::All),
"none" => Ok(Self::None),
// TODO: replace this error with a diagnostic
_ => Err("Value not supported for TrailingCommas"),
}
}
}
impl fmt::Display for TrailingCommas {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
TrailingCommas::Es5 => "ES5",
TrailingCommas::All => "All",
TrailingCommas::None => "None",
};
f.write_str(s)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum AttributePosition {
#[default]
+1 -1
View File
@@ -58,7 +58,7 @@ pub use formatter::Formatter;
pub use group_id::{GroupId, UniqueGroupIdBuilder};
pub use options::{
BracketSpacing, Expand, IndentStyle, IndentWidth, IndentWidthFromIntError, LineEnding,
LineWidth, LineWidthFromIntError, ParseFormatNumberError,
LineWidth, LineWidthFromIntError, ParseFormatNumberError, TrailingCommas,
};
pub use printer::{PrintResult, PrintWidth, Printed, Printer, PrinterOptions};
pub(crate) use simple_context::SimpleFormatContext;
+55
View File
@@ -378,3 +378,58 @@ impl fmt::Display for Expand {
f.write_str(s)
}
}
/// Print trailing commas wherever possible in multi-line comma-separated syntactic structures.
/// Mirrors Prettier's `trailingComma` option.
///
/// The `All` vs `Es5` distinction only matters for languages with constructs beyond ES5 (e.g. function, type parameters).
/// For JSON-family languages, both behave identically.
#[derive(Clone, Copy, Default, Debug, Eq, Hash, PartialEq)]
pub enum TrailingCommas {
/// Trailing commas wherever possible (including function parameters and calls).
#[default]
All,
/// Trailing commas where valid in ES5 (objects, arrays, etc.). No trailing commas in type parameters in TypeScript.
Es5,
/// No trailing commas.
None,
}
impl TrailingCommas {
pub const fn is_es5(self) -> bool {
matches!(self, TrailingCommas::Es5)
}
pub const fn is_all(self) -> bool {
matches!(self, TrailingCommas::All)
}
pub const fn is_none(self) -> bool {
matches!(self, TrailingCommas::None)
}
}
impl FromStr for TrailingCommas {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"es5" => Ok(Self::Es5),
"all" => Ok(Self::All),
"none" => Ok(Self::None),
// TODO: replace this error with a diagnostic
_ => Err("Value not supported for TrailingCommas"),
}
}
}
impl fmt::Display for TrailingCommas {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
TrailingCommas::Es5 => "ES5",
TrailingCommas::All => "All",
TrailingCommas::None => "None",
};
f.write_str(s)
}
}
+25 -3
View File
@@ -1,6 +1,6 @@
use oxc_formatter_core::{
BracketSpacing, Expand, FormatOptions, IndentStyle, IndentWidth, LineEnding, LineWidth,
PrinterOptions,
PrinterOptions, TrailingCommas,
};
/// JSON parser variant.
@@ -17,7 +17,7 @@ pub enum JsonVariant {
#[default]
Json,
/// Prettier's `parser: jsonc` equivalent.
/// Output: double-quoted strings, quoted object keys;
/// Output: double-quoted strings, quoted object keys,
/// trailing commas follow the user option.
/// Empty input is allowed.
Jsonc,
@@ -26,7 +26,7 @@ pub enum JsonVariant {
/// string quote style and trailing commas follow user options.
Json5,
/// Prettier's `parser: json-stringify` equivalent.
/// Output: `JSON.parse()`-compatible.
/// Output: `JSON.parse()`-compatible,
/// double-quoted strings, quoted keys, no trailing commas,
/// always pretty-printed with hard line breaks between entries.
/// The only variant that rejects comments at parse time.
@@ -42,6 +42,28 @@ pub struct JsonFormatOptions {
pub variant: JsonVariant,
pub bracket_spacing: BracketSpacing,
pub expand: Expand,
pub trailing_commas: TrailingCommas,
}
impl JsonFormatOptions {
/// Whether a trailing comma may follow the last entry of a multi-line object/array,
/// 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
///
/// `jsonc` and `json5` follow the user option (`all`/`es5` both emit, `none` does not).
/// Both go through Prettier's shared `estree` printer with `shouldPrintTrailingComma` fixed at the `es5` level,
/// which is why `all` and `es5` are indistinguishable here.
pub fn allow_trailing_comma(&self) -> bool {
match self.variant {
JsonVariant::Json | JsonVariant::JsonStringify => false,
JsonVariant::Jsonc | JsonVariant::Json5 => !self.trailing_commas.is_none(),
}
}
}
impl FormatOptions for JsonFormatOptions {
+32 -15
View File
@@ -1,8 +1,9 @@
use oxc_ast::ast::{ArrayExpression, ArrayExpressionElement, Expression};
use oxc_formatter_core::{
Buffer, Format,
Buffer, Format, FormatContext,
builders::{
block_indent, empty_line, group, soft_block_indent, soft_line_break_or_space, text,
block_indent, empty_line, group, if_group_breaks, soft_block_indent,
soft_line_break_or_space, text,
},
write,
};
@@ -26,14 +27,15 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonArray<'a, '_> {
if self.array.elements.is_empty() {
let dangling = f.context().comments().take_before(self.array.span.end);
if dangling.is_empty() {
write!(f, "]");
return;
if !dangling.is_empty() {
write!(
f,
[block_indent(&format_with(move |f| {
write_dangling_comments(dangling, f);
}))]
);
}
let inner = format_with(move |f: &mut JsonFormatter<'_, 'a>| {
write_dangling_comments(dangling, f);
});
write!(f, [block_indent(&inner), "]"]);
write!(f, "]");
return;
}
@@ -73,13 +75,19 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonArray<'a, '_> {
let elements = &self.array.elements;
let source = f.context().source_text();
let last_idx = elements.len() - 1;
let body = format_with(|f: &mut JsonFormatter<'_, 'a>| {
let allow_trailing = f.context().options().allow_trailing_comma();
// The trailing comma sits on the last fill item,
// so its `if_group_breaks` must reference the outer array group explicitly.
// Which is inside a `fill`, the nearest enclosing group isn't the array group.
// Only allocated when a trailing comma is possible (otherwise no group needs an id).
let group_id = allow_trailing.then(|| f.state().group_id("json-concise-array"));
let body = format_with(move |f| {
let mut filler = f.fill();
let mut prev_end: Option<u32> = None;
for (i, element) in elements.iter().enumerate() {
let curr_start = element.span().start;
let pe = prev_end;
let sep = format_with(move |f: &mut JsonFormatter<'_, 'a>| {
let sep = format_with(move |f| {
if let Some(pe_val) = pe {
let between = source.bytes_range(pe_val, curr_start);
if blank_line_after_comma(between) {
@@ -89,10 +97,12 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonArray<'a, '_> {
}
}
});
let item = format_with(move |f: &mut JsonFormatter<'_, 'a>| {
let item = format_with(move |f| {
write_array_element(element, f);
if i < last_idx {
write!(f, ",");
} else if allow_trailing {
write!(f, if_group_breaks(&",").with_group_id(group_id));
}
});
filler.entry(&sep, &item);
@@ -111,13 +121,20 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonArray<'a, '_> {
}
);
});
write!(f, [group(&soft_block_indent(&body)), "]"]);
write!(f, [group(&soft_block_indent(&body)).with_group_id(group_id), "]"]);
return;
}
let spans: Vec<_> = self.array.elements.iter().map(oxc_span::GetSpan::span).collect();
let elements = format_with(|f: &mut JsonFormatter<'_, 'a>| {
write_separated(f, &spans, TrailingSeparator::Disallowed, |i, f| {
let elements = format_with(|f| {
// An elision-forced trailing comma is load-bearing and always literal,
// so it overrides the variant's group-break separator.
let sep = if trailing_comma {
TrailingSeparator::Disallowed
} else {
TrailingSeparator::when_breaking(f.context().options().allow_trailing_comma())
};
write_separated(f, &spans, sep, self.array.span.end, |i, f| {
write_array_element(&self.array.elements[i], f);
});
@@ -37,7 +37,7 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonObject<'a, '_> {
write!(f, "}");
return;
}
let inner = format_with(move |f: &mut JsonFormatter<'_, 'a>| {
let inner = format_with(move |f| {
write_dangling_comments(dangling, f);
});
write!(f, [block_indent(&inner), "}"]);
@@ -46,8 +46,10 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonObject<'a, '_> {
// Collect property spans up-front for blank-line detection
let spans: Vec<_> = self.object.properties.iter().map(oxc_span::GetSpan::span).collect();
let properties = format_with(|f: &mut JsonFormatter<'_, 'a>| {
write_separated(f, &spans, TrailingSeparator::Disallowed, |i, f| {
let trailing =
TrailingSeparator::when_breaking(f.context().options().allow_trailing_comma());
let properties = format_with(|f| {
write_separated(f, &spans, trailing, self.object.span.end, |i, f| {
let property = &self.object.properties[i];
match property {
ObjectPropertyKind::ObjectProperty(prop) => {
+45 -3
View File
@@ -21,14 +21,23 @@ use crate::{
/// Whether a trailing `,` should follow the last entry.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum TrailingSeparator {
/// Never emit a trailing separator. Used for the `json` variant.
/// Never emit a trailing separator.
/// Used for the `json` / `json-stringify` variants (and elision-forced arrays).
#[default]
Disallowed,
/// Emit `,` only when the enclosing group breaks (multi-line).
#[expect(dead_code)] // Reserved for future `jsonc` / `json5` variants.
/// Used for the `jsonc` / `json5` variants when the user option allows it.
AllowedWhenBreaking,
}
impl TrailingSeparator {
/// `AllowedWhenBreaking` when `allow` (the variant + user option permit a trailing comma),
/// otherwise `Disallowed`.
pub fn when_breaking(allow: bool) -> Self {
if allow { Self::AllowedWhenBreaking } else { Self::Disallowed }
}
}
/// Writes entries with `, ` (or a soft line break) between them.
/// If the source between consecutive entries contains a blank line,
/// that blank line is preserved in the output.
@@ -40,6 +49,7 @@ pub fn write_separated<'a, F>(
f: &mut JsonFormatter<'_, 'a>,
spans: &[Span],
trailing: TrailingSeparator,
upper_bound: u32,
mut emit_entry: F,
) where
F: FnMut(usize, &mut JsonFormatter<'_, 'a>),
@@ -56,12 +66,44 @@ pub fn write_separated<'a, F>(
match trailing {
TrailingSeparator::Disallowed => {}
TrailingSeparator::AllowedWhenBreaking => {
write!(f, if_group_breaks(&","));
write_trailing_separator(upper_bound, f);
}
}
}
}
/// Emits the optional trailing `,` for the last entry,
/// threading the last entry's same-line trailing block comments before the comma.
///
/// Mirrors [`write_inter_entry_separator`]'s rule applied to the final position:
/// a block comment that sits on the same line as the last value is part of that value's trailing,
/// so it prints before the separator (`value /* block */,`).
/// Line comments and own-line comments stay pending for the caller's [`crate::comments::FormatTrailingInsideComments`] pass.
/// Landing after the comma (`value, // line`) which is correct,
/// since a line comment terminates the line.
///
/// `upper_bound` (the container's closing-delimiter position) bounds the comment scan
/// so nested/outer comments aren't pulled in.
fn write_trailing_separator(upper_bound: u32, f: &mut JsonFormatter<'_, '_>) {
// Leading run of same-line block comments (no preceding newline, not a line comment).
let block_end = f
.context()
.comments()
.iter_before(upper_bound)
.take_while(|c| !c.preceded_by_newline() && !c.is_line())
.last()
.map(|c| c.span.end);
if let Some(block_end) = block_end {
for c in f.context().comments().take_before(block_end) {
write!(f, space());
write_single_comment(c, f);
}
}
write!(f, if_group_breaks(&","));
}
/// Writes the `,` separator between two entries,
/// threading any "trailing-of-prev" comments around the comma.
///
@@ -0,0 +1 @@
[1, 2 /* block */]
@@ -0,0 +1,28 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
[1, 2 /* block */]
==================== Output ====================
------------------------------------
{ printWidth: 80, variant: "jsonc" }
------------------------------------
[1, 2 /* block */]
-------------------------------------
{ printWidth: 100, variant: "jsonc" }
-------------------------------------
[1, 2 /* block */]
-----------------------------------------------------------
{ printWidth: 80, trailingComma: "none", variant: "jsonc" }
-----------------------------------------------------------
[1, 2 /* block */]
------------------------------------------------------------
{ printWidth: 100, trailingComma: "none", variant: "jsonc" }
------------------------------------------------------------
[1, 2 /* block */]
===================== End =====================
@@ -0,0 +1,4 @@
[
1,
2 // last element trailing comment
]
@@ -0,0 +1,43 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
[
1,
2 // last element trailing comment
]
==================== Output ====================
------------------------------------
{ printWidth: 80, variant: "jsonc" }
------------------------------------
[
1,
2, // last element trailing comment
]
-------------------------------------
{ printWidth: 100, variant: "jsonc" }
-------------------------------------
[
1,
2, // last element trailing comment
]
-----------------------------------------------------------
{ printWidth: 80, trailingComma: "none", variant: "jsonc" }
-----------------------------------------------------------
[
1,
2 // last element trailing comment
]
------------------------------------------------------------
{ printWidth: 100, trailingComma: "none", variant: "jsonc" }
------------------------------------------------------------
[
1,
2 // last element trailing comment
]
===================== End =====================
@@ -0,0 +1,4 @@
{
"a": 1,
"b": 2 /* block comment */,
}
@@ -0,0 +1,43 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
{
"a": 1,
"b": 2 /* block comment */,
}
==================== Output ====================
------------------------------------
{ printWidth: 80, variant: "jsonc" }
------------------------------------
{
"a": 1,
"b": 2 /* block comment */,
}
-------------------------------------
{ printWidth: 100, variant: "jsonc" }
-------------------------------------
{
"a": 1,
"b": 2 /* block comment */,
}
-----------------------------------------------------------
{ printWidth: 80, trailingComma: "none", variant: "jsonc" }
-----------------------------------------------------------
{
"a": 1,
"b": 2 /* block comment */
}
------------------------------------------------------------
{ printWidth: 100, trailingComma: "none", variant: "jsonc" }
------------------------------------------------------------
{
"a": 1,
"b": 2 /* block comment */
}
===================== End =====================
@@ -0,0 +1,9 @@
{
"a": 1,
"b": 2, // trailing line comment on last prop
"workspaces": {
"packages/test-dev-server/tests/fixtures/*": {
"entry": ["dev.config.mjs", "src/*.js"] // long comment keeps the line > 80, but the value stays inline
}
}
}
@@ -0,0 +1,68 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
{
"a": 1,
"b": 2, // trailing line comment on last prop
"workspaces": {
"packages/test-dev-server/tests/fixtures/*": {
"entry": ["dev.config.mjs", "src/*.js"] // long comment keeps the line > 80, but the value stays inline
}
}
}
==================== Output ====================
------------------------------------
{ printWidth: 80, variant: "jsonc" }
------------------------------------
{
"a": 1,
"b": 2, // trailing line comment on last prop
"workspaces": {
"packages/test-dev-server/tests/fixtures/*": {
"entry": ["dev.config.mjs", "src/*.js"], // long comment keeps the line > 80, but the value stays inline
},
},
}
-------------------------------------
{ printWidth: 100, variant: "jsonc" }
-------------------------------------
{
"a": 1,
"b": 2, // trailing line comment on last prop
"workspaces": {
"packages/test-dev-server/tests/fixtures/*": {
"entry": ["dev.config.mjs", "src/*.js"], // long comment keeps the line > 80, but the value stays inline
},
},
}
-----------------------------------------------------------
{ printWidth: 80, trailingComma: "none", variant: "jsonc" }
-----------------------------------------------------------
{
"a": 1,
"b": 2, // trailing line comment on last prop
"workspaces": {
"packages/test-dev-server/tests/fixtures/*": {
"entry": ["dev.config.mjs", "src/*.js"] // long comment keeps the line > 80, but the value stays inline
}
}
}
------------------------------------------------------------
{ printWidth: 100, trailingComma: "none", variant: "jsonc" }
------------------------------------------------------------
{
"a": 1,
"b": 2, // trailing line comment on last prop
"workspaces": {
"packages/test-dev-server/tests/fixtures/*": {
"entry": ["dev.config.mjs", "src/*.js"] // long comment keeps the line > 80, but the value stays inline
}
}
}
===================== End =====================
@@ -0,0 +1 @@
// this file is only a comment
@@ -0,0 +1,28 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
// this file is only a comment
==================== Output ====================
------------------------------------
{ printWidth: 80, variant: "jsonc" }
------------------------------------
// this file is only a comment
-------------------------------------
{ printWidth: 100, variant: "jsonc" }
-------------------------------------
// this file is only a comment
-----------------------------------------------------------
{ printWidth: 80, trailingComma: "none", variant: "jsonc" }
-----------------------------------------------------------
// this file is only a comment
------------------------------------------------------------
{ printWidth: 100, trailingComma: "none", variant: "jsonc" }
------------------------------------------------------------
// this file is only a comment
===================== End =====================
@@ -0,0 +1 @@
[ /* dangling */ ]
@@ -0,0 +1,36 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
[ /* dangling */ ]
==================== Output ====================
------------------------------------
{ printWidth: 80, variant: "jsonc" }
------------------------------------
[
/* dangling */
]
-------------------------------------
{ printWidth: 100, variant: "jsonc" }
-------------------------------------
[
/* dangling */
]
-----------------------------------------------------------
{ printWidth: 80, trailingComma: "none", variant: "jsonc" }
-----------------------------------------------------------
[
/* dangling */
]
------------------------------------------------------------
{ printWidth: 100, trailingComma: "none", variant: "jsonc" }
------------------------------------------------------------
[
/* dangling */
]
===================== End =====================
@@ -0,0 +1,11 @@
{
"matrix": [
[1, 2, 3],
[4, 5, 6]
],
"deep": {
"inner": {
"x": 1
}
}
}
@@ -0,0 +1,78 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
{
"matrix": [
[1, 2, 3],
[4, 5, 6]
],
"deep": {
"inner": {
"x": 1
}
}
}
==================== Output ====================
------------------------------------
{ printWidth: 80, variant: "jsonc" }
------------------------------------
{
"matrix": [
[1, 2, 3],
[4, 5, 6],
],
"deep": {
"inner": {
"x": 1,
},
},
}
-------------------------------------
{ printWidth: 100, variant: "jsonc" }
-------------------------------------
{
"matrix": [
[1, 2, 3],
[4, 5, 6],
],
"deep": {
"inner": {
"x": 1,
},
},
}
-----------------------------------------------------------
{ printWidth: 80, trailingComma: "none", variant: "jsonc" }
-----------------------------------------------------------
{
"matrix": [
[1, 2, 3],
[4, 5, 6]
],
"deep": {
"inner": {
"x": 1
}
}
}
------------------------------------------------------------
{ printWidth: 100, trailingComma: "none", variant: "jsonc" }
------------------------------------------------------------
{
"matrix": [
[1, 2, 3],
[4, 5, 6]
],
"deep": {
"inner": {
"x": 1
}
}
}
===================== End =====================
@@ -0,0 +1 @@
[{ "variant": "jsonc" }, { "variant": "jsonc", "trailingComma": "none" }]
@@ -0,0 +1,4 @@
{
unquoted: 'single quoted value',
"normal": "double"
}
@@ -0,0 +1,43 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
{
unquoted: 'single quoted value',
"normal": "double"
}
==================== Output ====================
------------------------------------
{ printWidth: 80, variant: "jsonc" }
------------------------------------
{
"unquoted": "single quoted value",
"normal": "double",
}
-------------------------------------
{ printWidth: 100, variant: "jsonc" }
-------------------------------------
{
"unquoted": "single quoted value",
"normal": "double",
}
-----------------------------------------------------------
{ printWidth: 80, trailingComma: "none", variant: "jsonc" }
-----------------------------------------------------------
{
"unquoted": "single quoted value",
"normal": "double"
}
------------------------------------------------------------
{ printWidth: 100, trailingComma: "none", variant: "jsonc" }
------------------------------------------------------------
{
"unquoted": "single quoted value",
"normal": "double"
}
===================== End =====================
@@ -0,0 +1 @@
[100000, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, 1000000]
@@ -0,0 +1,34 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
[100000, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, 1000000]
==================== Output ====================
------------------------------------
{ printWidth: 80, variant: "jsonc" }
------------------------------------
[
100000, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000,
1000000,
]
-------------------------------------
{ printWidth: 100, variant: "jsonc" }
-------------------------------------
[100000, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, 1000000]
-----------------------------------------------------------
{ printWidth: 80, trailingComma: "none", variant: "jsonc" }
-----------------------------------------------------------
[
100000, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000,
1000000
]
------------------------------------------------------------
{ printWidth: 100, trailingComma: "none", variant: "jsonc" }
------------------------------------------------------------
[100000, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, 1000000]
===================== End =====================
@@ -0,0 +1,8 @@
// jsonc allows comments and (per option) trailing commas
{
"a": 1,
"b": [1, 2, 3],
"nested": {
"x": true
}
}
@@ -0,0 +1,63 @@
---
source: crates/oxc_formatter_json/tests/fixtures/mod.rs
---
==================== Input ====================
// jsonc allows comments and (per option) trailing commas
{
"a": 1,
"b": [1, 2, 3],
"nested": {
"x": true
}
}
==================== Output ====================
------------------------------------
{ printWidth: 80, variant: "jsonc" }
------------------------------------
// jsonc allows comments and (per option) trailing commas
{
"a": 1,
"b": [1, 2, 3],
"nested": {
"x": true,
},
}
-------------------------------------
{ printWidth: 100, variant: "jsonc" }
-------------------------------------
// jsonc allows comments and (per option) trailing commas
{
"a": 1,
"b": [1, 2, 3],
"nested": {
"x": true,
},
}
-----------------------------------------------------------
{ printWidth: 80, trailingComma: "none", variant: "jsonc" }
-----------------------------------------------------------
// jsonc allows comments and (per option) trailing commas
{
"a": 1,
"b": [1, 2, 3],
"nested": {
"x": true
}
}
------------------------------------------------------------
{ printWidth: 100, trailingComma: "none", variant: "jsonc" }
------------------------------------------------------------
// jsonc allows comments and (per option) trailing commas
{
"a": 1,
"b": [1, 2, 3],
"nested": {
"x": true
}
}
===================== End =====================
+9 -2
View File
@@ -1,8 +1,8 @@
use std::path::Path;
use std::{path::Path, str::FromStr};
use oxc_allocator::Allocator;
use oxc_formatter_core::{
BracketSpacing, Expand, IndentStyle, IndentWidth, LineEnding, LineWidth,
BracketSpacing, Expand, IndentStyle, IndentWidth, LineEnding, LineWidth, TrailingCommas,
test_support::{FixtureFormatter, OptionSet, build_fixture_snapshot},
};
use oxc_formatter_json::{JsonFormatOptions, JsonVariant, format};
@@ -58,6 +58,13 @@ impl FixtureFormatter for JsonHarness {
};
}
}
"trailingComma" => {
if let Some(s) = value.as_str()
&& let Ok(trailing_commas) = TrailingCommas::from_str(s)
{
options.trailing_commas = trailing_commas;
}
}
"bracketSpacing" => {
if let Some(b) = value.as_bool() {
options.bracket_spacing = BracketSpacing::from(b);
@@ -0,0 +1,6 @@
jsonc compatibility: 9/9 (100.00%)
# Failed
| Spec path | Failed or Passed | Match ratio |
| :-------- | :--------------: | :---------: |
+1 -1
View File
@@ -14,7 +14,7 @@ fn main() {
let debug = args.contains("--debug");
let filter: Option<String> = args.opt_value_from_str("--filter").unwrap();
for language in [TestLanguage::Js, TestLanguage::Ts, TestLanguage::Json] {
for language in [TestLanguage::Js, TestLanguage::Ts, TestLanguage::Json, TestLanguage::Jsonc] {
TestRunner::new(TestRunnerOptions { language, debug, filter: filter.clone() }).run();
}
JsdocTestRunner::new(filter, debug).run();
+14 -7
View File
@@ -13,6 +13,7 @@ pub enum TestLanguage {
Js,
Ts,
Json,
Jsonc,
}
impl TestLanguage {
@@ -21,6 +22,7 @@ impl TestLanguage {
Self::Js => "js",
Self::Ts => "ts",
Self::Json => "json",
Self::Jsonc => "jsonc",
}
}
@@ -31,16 +33,21 @@ 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<_>>(),
// Phase 1 of the JSON formatter targets the `json` parser only.
// Out-of-scope siblings:
// - `jsonc/*` / `json5-as-json-with-trailing-commas/` — jsonc/json5 parsers
// - `json-superset/` — inline `snippets` shape, not parseable by spec.rs
// - `range/` — range-formatting tests, not a whole-file format
// `with-comment/` is included because each of its `format.test.js`
// entries lists a parser explicitly; spec.rs filters out non-`json` ones.
// The JSON formatter targets the `json` parser.
// `with-comment/` is shared with `Jsonc`: each call lists its own parser,
// so `spec.rs` keeps only the `json` ones here.
// Out-of-scope (TODO) siblings:
// - `json5-as-json-with-trailing-commas/`: `json5` parser
// - `json-superset/`: inline `snippets` shape, not parseable by `spec.rs`
// - `range/`: range-formatting tests, not a whole-file format
Self::Json => {
vec![base.join("json").join("json"), base.join("json").join("with-comment")]
}
// The `jsonc` parser. `with-comment/` is shared with `Json` (see above);
// `spec.rs` keeps only the `jsonc` calls here.
Self::Jsonc => {
vec![base.join("json").join("jsonc"), base.join("json").join("with-comment")]
}
}
}
}
+17 -9
View File
@@ -134,11 +134,13 @@ impl VisitMut<'_> for SpecParser {
return;
}
// For the JSON language we accept only calls whose first parser is `"json"`.
// `json5` / `json-stringify` calls share the same `format.test.js` but are
// out of scope until later phases.
if matches!(self.language, TestLanguage::Json) && !parsers.iter().any(|p| p == "json") {
return;
// The `json` / `jsonc` languages each accept only their own parser's calls.
// A single `format.test.js` may list several parsers (e.g. `with-comment/`),
// so we filter per-language; `json5` / `json-stringify` stay 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,
_ => {}
}
let mut js_options = JsFormatOptions {
@@ -146,9 +148,14 @@ impl VisitMut<'_> for SpecParser {
line_width: LineWidth::try_from(80).unwrap(),
..Default::default()
};
// The variant follows the language: `Json` -> `json`, `Jsonc` -> `jsonc`.
let variant = match self.language {
TestLanguage::Jsonc => JsonVariant::Jsonc,
_ => JsonVariant::Json,
};
let mut json_options = JsonFormatOptions {
line_width: LineWidth::try_from(80).unwrap(),
variant: JsonVariant::Json,
variant,
..Default::default()
};
@@ -222,8 +229,9 @@ impl VisitMut<'_> for SpecParser {
let s = literal.value.as_str();
match name.as_ref() {
"trailingComma" => {
js_options.trailing_commas =
TrailingCommas::from_str(s).unwrap();
let trailing_commas = TrailingCommas::from_str(s).unwrap();
js_options.trailing_commas = trailing_commas;
json_options.trailing_commas = trailing_commas;
}
"endOfLine" => {
// TODO: change `unwrap_or_default` to `unwrap`
@@ -292,7 +300,7 @@ impl VisitMut<'_> for SpecParser {
snapshot_options.sort_by(|a, b| a.0.cmp(&b.0));
let options = match self.language {
TestLanguage::Json => SpecOptions::Json(json_options),
TestLanguage::Json | TestLanguage::Jsonc => SpecOptions::Json(json_options),
TestLanguage::Js | TestLanguage::Ts => SpecOptions::Js(Box::new(js_options)),
};
self.calls.push((options, snapshot_options));