mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
1 parent
beb46d31fa
commit
27a6db879b
33 files changed
+723
-100
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 */]
|
||||
+28
@@ -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
|
||||
]
|
||||
+43
@@ -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 */,
|
||||
}
|
||||
+43
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -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" }]
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
unquoted: 'single quoted value',
|
||||
"normal": "double"
|
||||
}
|
||||
+43
@@ -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 =====================
|
||||
+1
@@ -0,0 +1 @@
|
||||
[100000, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, 1000000]
|
||||
+34
@@ -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
|
||||
}
|
||||
}
|
||||
+63
@@ -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
@@ -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 |
|
||||
| :-------- | :--------------: | :---------: |
|
||||
@@ -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();
|
||||
|
||||
@@ -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")]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user