From fec2ed9dc6f991806ce03cf31152b3fffb466a7b Mon Sep 17 00:00:00 2001 From: leaysgur <6259812+leaysgur@users.noreply.github.com> Date: Thu, 16 Oct 2025 07:29:37 +0000 Subject: [PATCH] feat(oxfmt): Use Prettier style config key and value (#14612) Fixes #14591 `Oxfmtrc` format is now compatible with Prettier's configuration file. e.g. `{ "semicolons": "as-needed" }` -> `{ "semi": false }` Note about a few exceptions: - `endOfLine: "auto"` is not supported - `quoteProps: "consistent"` is not supported - `objectWrap: "always"` is also supported in addition to "preserve" and "collapse" --- .../tests/fixtures/config_file/.oxfmtrc.json | 2 +- .../oxfmt/tests/fixtures/config_file/fmt.json | 2 +- .../tests/fixtures/config_file/fmt.jsonc | 2 +- .../config_file/nested/.oxfmtrc.jsonc | 2 +- crates/oxc_formatter/src/service/oxfmtrc.rs | 161 ++++++++++++------ .../tests/snapshots/schema_json.snap | 55 +++--- .../formatter/custom_config_path/format.json | 2 +- .../formatter/root_config/.oxfmtrc.json | 2 +- npm/oxfmt/configuration_schema.json | 55 +++--- 9 files changed, 175 insertions(+), 108 deletions(-) diff --git a/apps/oxfmt/tests/fixtures/config_file/.oxfmtrc.json b/apps/oxfmt/tests/fixtures/config_file/.oxfmtrc.json index 9e85b9792adaf..732e2200080f7 100644 --- a/apps/oxfmt/tests/fixtures/config_file/.oxfmtrc.json +++ b/apps/oxfmt/tests/fixtures/config_file/.oxfmtrc.json @@ -1,3 +1,3 @@ { - "semicolons": "always" + "semi": true } diff --git a/apps/oxfmt/tests/fixtures/config_file/fmt.json b/apps/oxfmt/tests/fixtures/config_file/fmt.json index 9e85b9792adaf..732e2200080f7 100644 --- a/apps/oxfmt/tests/fixtures/config_file/fmt.json +++ b/apps/oxfmt/tests/fixtures/config_file/fmt.json @@ -1,3 +1,3 @@ { - "semicolons": "always" + "semi": true } diff --git a/apps/oxfmt/tests/fixtures/config_file/fmt.jsonc b/apps/oxfmt/tests/fixtures/config_file/fmt.jsonc index 3fe6912d9f490..094cc1415c770 100644 --- a/apps/oxfmt/tests/fixtures/config_file/fmt.jsonc +++ b/apps/oxfmt/tests/fixtures/config_file/fmt.jsonc @@ -1,4 +1,4 @@ { // Supports JSONC! - "semicolons": "always" + "semi": true } diff --git a/apps/oxfmt/tests/fixtures/config_file/nested/.oxfmtrc.jsonc b/apps/oxfmt/tests/fixtures/config_file/nested/.oxfmtrc.jsonc index 9e85b9792adaf..732e2200080f7 100644 --- a/apps/oxfmt/tests/fixtures/config_file/nested/.oxfmtrc.jsonc +++ b/apps/oxfmt/tests/fixtures/config_file/nested/.oxfmtrc.jsonc @@ -1,3 +1,3 @@ { - "semicolons": "always" + "semi": true } diff --git a/crates/oxc_formatter/src/service/oxfmtrc.rs b/crates/oxc_formatter/src/service/oxfmtrc.rs index 99e4ee345a04c..29f8fe5b9f2c5 100644 --- a/crates/oxc_formatter/src/service/oxfmtrc.rs +++ b/crates/oxc_formatter/src/service/oxfmtrc.rs @@ -9,39 +9,43 @@ use crate::{ Semicolons, SortImports, SortOrder, TrailingCommas, }; +/// Configuration options for the formatter. +/// Most options are the same as Prettier's options. +/// See also #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", default)] pub struct Oxfmtrc { #[serde(skip_serializing_if = "Option::is_none")] - pub indent_style: Option, + pub use_tabs: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub indent_width: Option, + pub tab_width: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub line_ending: Option, + pub end_of_line: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub line_width: Option, + pub print_width: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub quote_style: Option, + pub single_quote: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub jsx_quote_style: Option, + pub jsx_single_quote: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub quote_properties: Option, + pub quote_props: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub trailing_commas: Option, + pub trailing_comma: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub semicolons: Option, + pub semi: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub arrow_parentheses: Option, + pub arrow_parens: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bracket_spacing: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bracket_same_line: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub attribute_position: Option, + pub object_wrap: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub expand: Option, + pub single_attribute_per_line: Option, #[serde(skip_serializing_if = "Option::is_none")] pub experimental_operator_position: Option, + // TODO: experimental_ternaries #[serde(skip_serializing_if = "Option::is_none")] pub experimental_sort_imports: Option, } @@ -106,84 +110,112 @@ impl Oxfmtrc { pub fn into_format_options(self) -> Result { let mut options = FormatOptions::default(); - if let Some(style) = self.indent_style { - options.indent_style = - style.parse::().map_err(|e| format!("Invalid indent_style: {e}"))?; + // [Prettier] useTabs: boolean + if let Some(use_tabs) = self.use_tabs { + options.indent_style = if use_tabs { IndentStyle::Tab } else { IndentStyle::Space }; } - if let Some(width) = self.indent_width { + // [Prettier] tabWidth: number + if let Some(width) = self.tab_width { options.indent_width = - IndentWidth::try_from(width).map_err(|e| format!("Invalid indent_width: {e}"))?; + IndentWidth::try_from(width).map_err(|e| format!("Invalid tabWidth: {e}"))?; } - if let Some(ending) = self.line_ending { + // [Prettier] endOfLine: "lf" | "cr" | "crlf" | "auto" + // NOTE: "auto" is not supported + if let Some(ending) = self.end_of_line { options.line_ending = - ending.parse::().map_err(|e| format!("Invalid line_ending: {e}"))?; + ending.parse::().map_err(|e| format!("Invalid endOfLine: {e}"))?; } - if let Some(width) = self.line_width { + // [Prettier] printWidth: number + if let Some(width) = self.print_width { options.line_width = - LineWidth::try_from(width).map_err(|e| format!("Invalid line_width: {e}"))?; + LineWidth::try_from(width).map_err(|e| format!("Invalid printWidth: {e}"))?; } - if let Some(style) = self.quote_style { + // [Prettier] singleQuote: boolean + if let Some(single_quote) = self.single_quote { options.quote_style = - style.parse::().map_err(|e| format!("Invalid quote_style: {e}"))?; + if single_quote { QuoteStyle::Single } else { QuoteStyle::Double }; } - if let Some(style) = self.jsx_quote_style { + // [Prettier] jsxSingleQuote: boolean + if let Some(jsx_single_quote) = self.jsx_single_quote { options.jsx_quote_style = - style.parse::().map_err(|e| format!("Invalid jsx_quote_style: {e}"))?; + if jsx_single_quote { QuoteStyle::Single } else { QuoteStyle::Double }; } - if let Some(props) = self.quote_properties { - options.quote_properties = props - .parse::() - .map_err(|e| format!("Invalid quote_properties: {e}"))?; + // [Prettier] quoteProps: "as-needed" | "consistent" | "preserve" + // NOTE: "consistent" is not supported + if let Some(props) = self.quote_props { + options.quote_properties = + props.parse::().map_err(|e| format!("Invalid quoteProps: {e}"))?; } - if let Some(commas) = self.trailing_commas { + // [Prettier] trailingComma: "all" | "es5" | "none" + if let Some(commas) = self.trailing_comma { options.trailing_commas = commas .parse::() - .map_err(|e| format!("Invalid trailing_commas: {e}"))?; + .map_err(|e| format!("Invalid trailingComma: {e}"))?; } - if let Some(semis) = self.semicolons { - options.semicolons = - semis.parse::().map_err(|e| format!("Invalid semicolons: {e}"))?; + // [Prettier] semi: boolean -> Semicolons + if let Some(semi) = self.semi { + options.semicolons = if semi { Semicolons::Always } else { Semicolons::AsNeeded }; } - if let Some(parens) = self.arrow_parentheses { - options.arrow_parentheses = parens + // [Prettier] arrowParens: "avoid" | "always" + if let Some(parens) = self.arrow_parens { + let normalized = match parens.as_str() { + "avoid" => "as-needed", + _ => &parens, + }; + options.arrow_parentheses = normalized .parse::() - .map_err(|e| format!("Invalid arrow_parentheses: {e}"))?; + .map_err(|e| format!("Invalid arrowParens: {e}"))?; } + // [Prettier] bracketSpacing: boolean if let Some(spacing) = self.bracket_spacing { options.bracket_spacing = BracketSpacing::from(spacing); } + // [Prettier] bracketSameLine: boolean if let Some(same_line) = self.bracket_same_line { options.bracket_same_line = BracketSameLine::from(same_line); } - if let Some(position) = self.attribute_position { - options.attribute_position = position - .parse::() - .map_err(|e| format!("Invalid attribute_position: {e}"))?; + // [Prettier] singleAttributePerLine: boolean + if let Some(single_attribute_per_line) = self.single_attribute_per_line { + options.attribute_position = if single_attribute_per_line { + AttributePosition::Multiline + } else { + AttributePosition::Auto + }; } - if let Some(expand) = self.expand { + // [Prettier] objectWrap: "preserve" | "collapse" + // NOTE: In addition to Prettier, we also support "always" + if let Some(object_wrap) = self.object_wrap { + let normalized = match object_wrap.as_str() { + "preserve" => "auto", + "collapse" => "never", + _ => &object_wrap, + }; options.expand = - expand.parse::().map_err(|e| format!("Invalid expand: {e}"))?; + normalized.parse::().map_err(|e| format!("Invalid objectWrap: {e}"))?; } + // [Prettier] experimentalOperatorPosition: "start" | "end" if let Some(position) = self.experimental_operator_position { options.experimental_operator_position = position .parse::() .map_err(|e| format!("Invalid experimental_operator_position: {e}"))?; } + // Below are our own extensions + if let Some(sort_imports_config) = self.experimental_sort_imports { let order = sort_imports_config .order @@ -210,11 +242,11 @@ mod tests { #[test] fn test_config_parsing() { let json = r#"{ - "indentStyle": "tab", - "indentWidth": 4, - "lineWidth": 100, - "quoteStyle": "single", - "semicolons": "as-needed", + "useTabs": true, + "tabWidth": 4, + "printWidth": 100, + "singleQuote": true, + "semi": false, "experimentalSortImports": { "partitionByNewline": true, "order": "desc", @@ -228,6 +260,8 @@ mod tests { assert!(options.indent_style.is_tab()); assert_eq!(options.indent_width.value(), 4); assert_eq!(options.line_width.value(), 100); + assert!(!options.quote_style.is_double()); + assert!(options.semicolons.is_as_needed()); let sort_imports = options.experimental_sort_imports.unwrap(); assert!(sort_imports.partition_by_newline); @@ -264,4 +298,35 @@ mod tests { assert_eq!(options.line_width.value(), 80); assert_eq!(options.experimental_sort_imports, None); } + + #[test] + fn test_arrow_parens_normalization() { + // Test "avoid" -> "as-needed" normalization + let config: Oxfmtrc = serde_json::from_str(r#"{"arrowParens": "avoid"}"#).unwrap(); + let options = config.into_format_options().unwrap(); + assert!(options.arrow_parentheses.is_as_needed()); + + // Test "always" remains unchanged + let config: Oxfmtrc = serde_json::from_str(r#"{"arrowParens": "always"}"#).unwrap(); + let options = config.into_format_options().unwrap(); + assert!(options.arrow_parentheses.is_always()); + } + + #[test] + fn test_object_wrap_normalization() { + // Test "preserve" -> "auto" normalization + let config: Oxfmtrc = serde_json::from_str(r#"{"objectWrap": "preserve"}"#).unwrap(); + let options = config.into_format_options().unwrap(); + assert_eq!(options.expand, Expand::Auto); + + // Test "collapse" -> "never" normalization + let config: Oxfmtrc = serde_json::from_str(r#"{"objectWrap": "collapse"}"#).unwrap(); + let options = config.into_format_options().unwrap(); + assert_eq!(options.expand, Expand::Never); + + // Test "always" remains unchanged + let config: Oxfmtrc = serde_json::from_str(r#"{"objectWrap": "always"}"#).unwrap(); + let options = config.into_format_options().unwrap(); + assert_eq!(options.expand, Expand::Always); + } } diff --git a/crates/oxc_formatter/tests/snapshots/schema_json.snap b/crates/oxc_formatter/tests/snapshots/schema_json.snap index 355635e9090f0..efdb9761091a7 100644 --- a/crates/oxc_formatter/tests/snapshots/schema_json.snap +++ b/crates/oxc_formatter/tests/snapshots/schema_json.snap @@ -5,15 +5,10 @@ expression: json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Oxfmtrc", + "description": "Configuration options for the formatter.\nMost options are the same as Prettier's options.\nSee also ", "type": "object", "properties": { - "arrowParentheses": { - "type": [ - "string", - "null" - ] - }, - "attributePosition": { + "arrowParens": { "type": [ "string", "null" @@ -31,7 +26,7 @@ expression: json "null" ] }, - "expand": { + "endOfLine": { "type": [ "string", "null" @@ -53,61 +48,67 @@ expression: json } ] }, - "indentStyle": { + "jsxSingleQuote": { + "type": [ + "boolean", + "null" + ] + }, + "objectWrap": { "type": [ "string", "null" ] }, - "indentWidth": { + "printWidth": { "type": [ "integer", "null" ], - "format": "uint8", + "format": "uint16", "minimum": 0.0 }, - "jsxQuoteStyle": { + "quoteProps": { "type": [ "string", "null" ] }, - "lineEnding": { + "semi": { "type": [ - "string", + "boolean", "null" ] }, - "lineWidth": { + "singleAttributePerLine": { "type": [ - "integer", + "boolean", "null" - ], - "format": "uint16", - "minimum": 0.0 + ] }, - "quoteProperties": { + "singleQuote": { "type": [ - "string", + "boolean", "null" ] }, - "quoteStyle": { + "tabWidth": { "type": [ - "string", + "integer", "null" - ] + ], + "format": "uint8", + "minimum": 0.0 }, - "semicolons": { + "trailingComma": { "type": [ "string", "null" ] }, - "trailingCommas": { + "useTabs": { "type": [ - "string", + "boolean", "null" ] } diff --git a/crates/oxc_language_server/fixtures/formatter/custom_config_path/format.json b/crates/oxc_language_server/fixtures/formatter/custom_config_path/format.json index bf74bec6496f9..cce9d3c080177 100644 --- a/crates/oxc_language_server/fixtures/formatter/custom_config_path/format.json +++ b/crates/oxc_language_server/fixtures/formatter/custom_config_path/format.json @@ -1,3 +1,3 @@ { - "semicolons": "as-needed" + "semi": false } diff --git a/crates/oxc_language_server/fixtures/formatter/root_config/.oxfmtrc.json b/crates/oxc_language_server/fixtures/formatter/root_config/.oxfmtrc.json index bf74bec6496f9..cce9d3c080177 100644 --- a/crates/oxc_language_server/fixtures/formatter/root_config/.oxfmtrc.json +++ b/crates/oxc_language_server/fixtures/formatter/root_config/.oxfmtrc.json @@ -1,3 +1,3 @@ { - "semicolons": "as-needed" + "semi": false } diff --git a/npm/oxfmt/configuration_schema.json b/npm/oxfmt/configuration_schema.json index b0ccdccdbe309..f3e5a8e40e9ea 100644 --- a/npm/oxfmt/configuration_schema.json +++ b/npm/oxfmt/configuration_schema.json @@ -1,15 +1,10 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Oxfmtrc", + "description": "Configuration options for the formatter.\nMost options are the same as Prettier's options.\nSee also ", "type": "object", "properties": { - "arrowParentheses": { - "type": [ - "string", - "null" - ] - }, - "attributePosition": { + "arrowParens": { "type": [ "string", "null" @@ -27,7 +22,7 @@ "null" ] }, - "expand": { + "endOfLine": { "type": [ "string", "null" @@ -49,61 +44,67 @@ } ] }, - "indentStyle": { + "jsxSingleQuote": { + "type": [ + "boolean", + "null" + ] + }, + "objectWrap": { "type": [ "string", "null" ] }, - "indentWidth": { + "printWidth": { "type": [ "integer", "null" ], - "format": "uint8", + "format": "uint16", "minimum": 0.0 }, - "jsxQuoteStyle": { + "quoteProps": { "type": [ "string", "null" ] }, - "lineEnding": { + "semi": { "type": [ - "string", + "boolean", "null" ] }, - "lineWidth": { + "singleAttributePerLine": { "type": [ - "integer", + "boolean", "null" - ], - "format": "uint16", - "minimum": 0.0 + ] }, - "quoteProperties": { + "singleQuote": { "type": [ - "string", + "boolean", "null" ] }, - "quoteStyle": { + "tabWidth": { "type": [ - "string", + "integer", "null" - ] + ], + "format": "uint8", + "minimum": 0.0 }, - "semicolons": { + "trailingComma": { "type": [ "string", "null" ] }, - "trailingCommas": { + "useTabs": { "type": [ - "string", + "boolean", "null" ] }