Skip to content

Commit fec2ed9

Browse files
committed
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"
1 parent 9e36186 commit fec2ed9

File tree

9 files changed

+175
-108
lines changed

9 files changed

+175
-108
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"semicolons": "always"
2+
"semi": true
33
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"semicolons": "always"
2+
"semi": true
33
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
// Supports JSONC!
3-
"semicolons": "always"
3+
"semi": true
44
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"semicolons": "always"
2+
"semi": true
33
}

crates/oxc_formatter/src/service/oxfmtrc.rs

Lines changed: 113 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,43 @@ use crate::{
99
Semicolons, SortImports, SortOrder, TrailingCommas,
1010
};
1111

12+
/// Configuration options for the formatter.
13+
/// Most options are the same as Prettier's options.
14+
/// See also <https://prettier.io/docs/options>
1215
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
1316
#[serde(rename_all = "camelCase", default)]
1417
pub struct Oxfmtrc {
1518
#[serde(skip_serializing_if = "Option::is_none")]
16-
pub indent_style: Option<String>,
19+
pub use_tabs: Option<bool>,
1720
#[serde(skip_serializing_if = "Option::is_none")]
18-
pub indent_width: Option<u8>,
21+
pub tab_width: Option<u8>,
1922
#[serde(skip_serializing_if = "Option::is_none")]
20-
pub line_ending: Option<String>,
23+
pub end_of_line: Option<String>,
2124
#[serde(skip_serializing_if = "Option::is_none")]
22-
pub line_width: Option<u16>,
25+
pub print_width: Option<u16>,
2326
#[serde(skip_serializing_if = "Option::is_none")]
24-
pub quote_style: Option<String>,
27+
pub single_quote: Option<bool>,
2528
#[serde(skip_serializing_if = "Option::is_none")]
26-
pub jsx_quote_style: Option<String>,
29+
pub jsx_single_quote: Option<bool>,
2730
#[serde(skip_serializing_if = "Option::is_none")]
28-
pub quote_properties: Option<String>,
31+
pub quote_props: Option<String>,
2932
#[serde(skip_serializing_if = "Option::is_none")]
30-
pub trailing_commas: Option<String>,
33+
pub trailing_comma: Option<String>,
3134
#[serde(skip_serializing_if = "Option::is_none")]
32-
pub semicolons: Option<String>,
35+
pub semi: Option<bool>,
3336
#[serde(skip_serializing_if = "Option::is_none")]
34-
pub arrow_parentheses: Option<String>,
37+
pub arrow_parens: Option<String>,
3538
#[serde(skip_serializing_if = "Option::is_none")]
3639
pub bracket_spacing: Option<bool>,
3740
#[serde(skip_serializing_if = "Option::is_none")]
3841
pub bracket_same_line: Option<bool>,
3942
#[serde(skip_serializing_if = "Option::is_none")]
40-
pub attribute_position: Option<String>,
43+
pub object_wrap: Option<String>,
4144
#[serde(skip_serializing_if = "Option::is_none")]
42-
pub expand: Option<String>,
45+
pub single_attribute_per_line: Option<bool>,
4346
#[serde(skip_serializing_if = "Option::is_none")]
4447
pub experimental_operator_position: Option<String>,
48+
// TODO: experimental_ternaries
4549
#[serde(skip_serializing_if = "Option::is_none")]
4650
pub experimental_sort_imports: Option<SortImportsConfig>,
4751
}
@@ -106,84 +110,112 @@ impl Oxfmtrc {
106110
pub fn into_format_options(self) -> Result<FormatOptions, String> {
107111
let mut options = FormatOptions::default();
108112

109-
if let Some(style) = self.indent_style {
110-
options.indent_style =
111-
style.parse::<IndentStyle>().map_err(|e| format!("Invalid indent_style: {e}"))?;
113+
// [Prettier] useTabs: boolean
114+
if let Some(use_tabs) = self.use_tabs {
115+
options.indent_style = if use_tabs { IndentStyle::Tab } else { IndentStyle::Space };
112116
}
113117

114-
if let Some(width) = self.indent_width {
118+
// [Prettier] tabWidth: number
119+
if let Some(width) = self.tab_width {
115120
options.indent_width =
116-
IndentWidth::try_from(width).map_err(|e| format!("Invalid indent_width: {e}"))?;
121+
IndentWidth::try_from(width).map_err(|e| format!("Invalid tabWidth: {e}"))?;
117122
}
118123

119-
if let Some(ending) = self.line_ending {
124+
// [Prettier] endOfLine: "lf" | "cr" | "crlf" | "auto"
125+
// NOTE: "auto" is not supported
126+
if let Some(ending) = self.end_of_line {
120127
options.line_ending =
121-
ending.parse::<LineEnding>().map_err(|e| format!("Invalid line_ending: {e}"))?;
128+
ending.parse::<LineEnding>().map_err(|e| format!("Invalid endOfLine: {e}"))?;
122129
}
123130

124-
if let Some(width) = self.line_width {
131+
// [Prettier] printWidth: number
132+
if let Some(width) = self.print_width {
125133
options.line_width =
126-
LineWidth::try_from(width).map_err(|e| format!("Invalid line_width: {e}"))?;
134+
LineWidth::try_from(width).map_err(|e| format!("Invalid printWidth: {e}"))?;
127135
}
128136

129-
if let Some(style) = self.quote_style {
137+
// [Prettier] singleQuote: boolean
138+
if let Some(single_quote) = self.single_quote {
130139
options.quote_style =
131-
style.parse::<QuoteStyle>().map_err(|e| format!("Invalid quote_style: {e}"))?;
140+
if single_quote { QuoteStyle::Single } else { QuoteStyle::Double };
132141
}
133142

134-
if let Some(style) = self.jsx_quote_style {
143+
// [Prettier] jsxSingleQuote: boolean
144+
if let Some(jsx_single_quote) = self.jsx_single_quote {
135145
options.jsx_quote_style =
136-
style.parse::<QuoteStyle>().map_err(|e| format!("Invalid jsx_quote_style: {e}"))?;
146+
if jsx_single_quote { QuoteStyle::Single } else { QuoteStyle::Double };
137147
}
138148

139-
if let Some(props) = self.quote_properties {
140-
options.quote_properties = props
141-
.parse::<QuoteProperties>()
142-
.map_err(|e| format!("Invalid quote_properties: {e}"))?;
149+
// [Prettier] quoteProps: "as-needed" | "consistent" | "preserve"
150+
// NOTE: "consistent" is not supported
151+
if let Some(props) = self.quote_props {
152+
options.quote_properties =
153+
props.parse::<QuoteProperties>().map_err(|e| format!("Invalid quoteProps: {e}"))?;
143154
}
144155

145-
if let Some(commas) = self.trailing_commas {
156+
// [Prettier] trailingComma: "all" | "es5" | "none"
157+
if let Some(commas) = self.trailing_comma {
146158
options.trailing_commas = commas
147159
.parse::<TrailingCommas>()
148-
.map_err(|e| format!("Invalid trailing_commas: {e}"))?;
160+
.map_err(|e| format!("Invalid trailingComma: {e}"))?;
149161
}
150162

151-
if let Some(semis) = self.semicolons {
152-
options.semicolons =
153-
semis.parse::<Semicolons>().map_err(|e| format!("Invalid semicolons: {e}"))?;
163+
// [Prettier] semi: boolean -> Semicolons
164+
if let Some(semi) = self.semi {
165+
options.semicolons = if semi { Semicolons::Always } else { Semicolons::AsNeeded };
154166
}
155167

156-
if let Some(parens) = self.arrow_parentheses {
157-
options.arrow_parentheses = parens
168+
// [Prettier] arrowParens: "avoid" | "always"
169+
if let Some(parens) = self.arrow_parens {
170+
let normalized = match parens.as_str() {
171+
"avoid" => "as-needed",
172+
_ => &parens,
173+
};
174+
options.arrow_parentheses = normalized
158175
.parse::<ArrowParentheses>()
159-
.map_err(|e| format!("Invalid arrow_parentheses: {e}"))?;
176+
.map_err(|e| format!("Invalid arrowParens: {e}"))?;
160177
}
161178

179+
// [Prettier] bracketSpacing: boolean
162180
if let Some(spacing) = self.bracket_spacing {
163181
options.bracket_spacing = BracketSpacing::from(spacing);
164182
}
165183

184+
// [Prettier] bracketSameLine: boolean
166185
if let Some(same_line) = self.bracket_same_line {
167186
options.bracket_same_line = BracketSameLine::from(same_line);
168187
}
169188

170-
if let Some(position) = self.attribute_position {
171-
options.attribute_position = position
172-
.parse::<AttributePosition>()
173-
.map_err(|e| format!("Invalid attribute_position: {e}"))?;
189+
// [Prettier] singleAttributePerLine: boolean
190+
if let Some(single_attribute_per_line) = self.single_attribute_per_line {
191+
options.attribute_position = if single_attribute_per_line {
192+
AttributePosition::Multiline
193+
} else {
194+
AttributePosition::Auto
195+
};
174196
}
175197

176-
if let Some(expand) = self.expand {
198+
// [Prettier] objectWrap: "preserve" | "collapse"
199+
// NOTE: In addition to Prettier, we also support "always"
200+
if let Some(object_wrap) = self.object_wrap {
201+
let normalized = match object_wrap.as_str() {
202+
"preserve" => "auto",
203+
"collapse" => "never",
204+
_ => &object_wrap,
205+
};
177206
options.expand =
178-
expand.parse::<Expand>().map_err(|e| format!("Invalid expand: {e}"))?;
207+
normalized.parse::<Expand>().map_err(|e| format!("Invalid objectWrap: {e}"))?;
179208
}
180209

210+
// [Prettier] experimentalOperatorPosition: "start" | "end"
181211
if let Some(position) = self.experimental_operator_position {
182212
options.experimental_operator_position = position
183213
.parse::<OperatorPosition>()
184214
.map_err(|e| format!("Invalid experimental_operator_position: {e}"))?;
185215
}
186216

217+
// Below are our own extensions
218+
187219
if let Some(sort_imports_config) = self.experimental_sort_imports {
188220
let order = sort_imports_config
189221
.order
@@ -210,11 +242,11 @@ mod tests {
210242
#[test]
211243
fn test_config_parsing() {
212244
let json = r#"{
213-
"indentStyle": "tab",
214-
"indentWidth": 4,
215-
"lineWidth": 100,
216-
"quoteStyle": "single",
217-
"semicolons": "as-needed",
245+
"useTabs": true,
246+
"tabWidth": 4,
247+
"printWidth": 100,
248+
"singleQuote": true,
249+
"semi": false,
218250
"experimentalSortImports": {
219251
"partitionByNewline": true,
220252
"order": "desc",
@@ -228,6 +260,8 @@ mod tests {
228260
assert!(options.indent_style.is_tab());
229261
assert_eq!(options.indent_width.value(), 4);
230262
assert_eq!(options.line_width.value(), 100);
263+
assert!(!options.quote_style.is_double());
264+
assert!(options.semicolons.is_as_needed());
231265

232266
let sort_imports = options.experimental_sort_imports.unwrap();
233267
assert!(sort_imports.partition_by_newline);
@@ -264,4 +298,35 @@ mod tests {
264298
assert_eq!(options.line_width.value(), 80);
265299
assert_eq!(options.experimental_sort_imports, None);
266300
}
301+
302+
#[test]
303+
fn test_arrow_parens_normalization() {
304+
// Test "avoid" -> "as-needed" normalization
305+
let config: Oxfmtrc = serde_json::from_str(r#"{"arrowParens": "avoid"}"#).unwrap();
306+
let options = config.into_format_options().unwrap();
307+
assert!(options.arrow_parentheses.is_as_needed());
308+
309+
// Test "always" remains unchanged
310+
let config: Oxfmtrc = serde_json::from_str(r#"{"arrowParens": "always"}"#).unwrap();
311+
let options = config.into_format_options().unwrap();
312+
assert!(options.arrow_parentheses.is_always());
313+
}
314+
315+
#[test]
316+
fn test_object_wrap_normalization() {
317+
// Test "preserve" -> "auto" normalization
318+
let config: Oxfmtrc = serde_json::from_str(r#"{"objectWrap": "preserve"}"#).unwrap();
319+
let options = config.into_format_options().unwrap();
320+
assert_eq!(options.expand, Expand::Auto);
321+
322+
// Test "collapse" -> "never" normalization
323+
let config: Oxfmtrc = serde_json::from_str(r#"{"objectWrap": "collapse"}"#).unwrap();
324+
let options = config.into_format_options().unwrap();
325+
assert_eq!(options.expand, Expand::Never);
326+
327+
// Test "always" remains unchanged
328+
let config: Oxfmtrc = serde_json::from_str(r#"{"objectWrap": "always"}"#).unwrap();
329+
let options = config.into_format_options().unwrap();
330+
assert_eq!(options.expand, Expand::Always);
331+
}
267332
}

crates/oxc_formatter/tests/snapshots/schema_json.snap

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,10 @@ expression: json
55
{
66
"$schema": "http://json-schema.org/draft-07/schema#",
77
"title": "Oxfmtrc",
8+
"description": "Configuration options for the formatter.\nMost options are the same as Prettier's options.\nSee also <https://prettier.io/docs/options>",
89
"type": "object",
910
"properties": {
10-
"arrowParentheses": {
11-
"type": [
12-
"string",
13-
"null"
14-
]
15-
},
16-
"attributePosition": {
11+
"arrowParens": {
1712
"type": [
1813
"string",
1914
"null"
@@ -31,7 +26,7 @@ expression: json
3126
"null"
3227
]
3328
},
34-
"expand": {
29+
"endOfLine": {
3530
"type": [
3631
"string",
3732
"null"
@@ -53,61 +48,67 @@ expression: json
5348
}
5449
]
5550
},
56-
"indentStyle": {
51+
"jsxSingleQuote": {
52+
"type": [
53+
"boolean",
54+
"null"
55+
]
56+
},
57+
"objectWrap": {
5758
"type": [
5859
"string",
5960
"null"
6061
]
6162
},
62-
"indentWidth": {
63+
"printWidth": {
6364
"type": [
6465
"integer",
6566
"null"
6667
],
67-
"format": "uint8",
68+
"format": "uint16",
6869
"minimum": 0.0
6970
},
70-
"jsxQuoteStyle": {
71+
"quoteProps": {
7172
"type": [
7273
"string",
7374
"null"
7475
]
7576
},
76-
"lineEnding": {
77+
"semi": {
7778
"type": [
78-
"string",
79+
"boolean",
7980
"null"
8081
]
8182
},
82-
"lineWidth": {
83+
"singleAttributePerLine": {
8384
"type": [
84-
"integer",
85+
"boolean",
8586
"null"
86-
],
87-
"format": "uint16",
88-
"minimum": 0.0
87+
]
8988
},
90-
"quoteProperties": {
89+
"singleQuote": {
9190
"type": [
92-
"string",
91+
"boolean",
9392
"null"
9493
]
9594
},
96-
"quoteStyle": {
95+
"tabWidth": {
9796
"type": [
98-
"string",
97+
"integer",
9998
"null"
100-
]
99+
],
100+
"format": "uint8",
101+
"minimum": 0.0
101102
},
102-
"semicolons": {
103+
"trailingComma": {
103104
"type": [
104105
"string",
105106
"null"
106107
]
107108
},
108-
"trailingCommas": {
109+
"useTabs": {
109110
"type": [
110-
"string",
111+
"boolean",
111112
"null"
112113
]
113114
}

0 commit comments

Comments
 (0)