mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
feat(oxfmt): support experimentalOperatorPosition (#25643)
Fixes #16366 --- Option name requires `experimental` prefix for now. Values are: - `"start"` - `"end"`(default) This is also available on playground.
This commit is contained in:
@@ -91,7 +91,6 @@ These Prettier options are skipped during migration:
|
||||
| ------------------------------- | ------------------------------------------------ |
|
||||
| `endOfLine: "auto"` | Not supported. Use `"lf"` or `"crlf"` explicitly |
|
||||
| `experimentalTernaries` | Not supported in JS/TS files yet |
|
||||
| `experimentalOperatorPosition` | Not supported in JS/TS files yet |
|
||||
| `requirePragma`, `insertPragma` | Not supported |
|
||||
| `parser`, `filepath` | Not applicable to oxfmt |
|
||||
|
||||
|
||||
@@ -92,8 +92,8 @@ export async function runMigratePrettier() {
|
||||
console.error(` - "endOfLine: auto" is not supported, skipping...`);
|
||||
continue;
|
||||
}
|
||||
// Oxfmt does not support these experimental options yet
|
||||
if (key === "experimentalTernaries" || key === "experimentalOperatorPosition") {
|
||||
// Oxfmt does not support this experimental option yet
|
||||
if (key === "experimentalTernaries") {
|
||||
console.error(` - "${key}" is not supported yet`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export type ArrowParensConfig = "always" | "avoid";
|
||||
export type EmbeddedLanguageFormattingConfig = "auto" | "off";
|
||||
export type EndOfLineConfig = "lf" | "crlf" | "cr";
|
||||
export type OperatorPositionConfig = "start" | "end";
|
||||
export type HtmlWhitespaceSensitivityConfig = "css" | "strict" | "ignore";
|
||||
export type JsdocUserConfig = boolean | JsdocConfig;
|
||||
export type CommentLineStrategyConfig = "singleLine" | "multiline" | "keep";
|
||||
@@ -92,6 +93,14 @@ export interface Oxfmtrc {
|
||||
* - Overrides `.editorconfig.end_of_line`
|
||||
*/
|
||||
endOfLine?: EndOfLineConfig;
|
||||
/**
|
||||
* When expressions wrap lines, print operators at the start of new lines (`"start"`)
|
||||
* or at the end of previous lines (`"end"`).
|
||||
*
|
||||
* - Languages: JS, JSX, TS, TSX
|
||||
* - Default: `"end"`
|
||||
*/
|
||||
experimentalOperatorPosition?: OperatorPositionConfig;
|
||||
/**
|
||||
* Specify the global whitespace sensitivity for HTML, Vue, Angular, and Handlebars.
|
||||
*
|
||||
@@ -421,6 +430,14 @@ export interface FormatConfig {
|
||||
* - Overrides `.editorconfig.end_of_line`
|
||||
*/
|
||||
endOfLine?: EndOfLineConfig;
|
||||
/**
|
||||
* When expressions wrap lines, print operators at the start of new lines (`"start"`)
|
||||
* or at the end of previous lines (`"end"`).
|
||||
*
|
||||
* - Languages: JS, JSX, TS, TSX
|
||||
* - Default: `"end"`
|
||||
*/
|
||||
experimentalOperatorPosition?: OperatorPositionConfig;
|
||||
/**
|
||||
* Specify the global whitespace sensitivity for HTML, Vue, Angular, and Handlebars.
|
||||
*
|
||||
|
||||
@@ -3,8 +3,8 @@ use oxc_formatter::SortTailwindcssOptions;
|
||||
use oxc_formatter::{
|
||||
ArrowParentheses, AttributePosition, BracketSameLine, BracketSpacing, CommentLineStrategy,
|
||||
CustomGroupDefinition, Expand, GroupEntry, ImportModifier, ImportSelector, JsFormatOptions,
|
||||
JsdocOptions, LineWrappingStyle, QuoteProperties, QuoteStyle, Semicolons, SortImportsOptions,
|
||||
SortOrder, TrailingCommas,
|
||||
JsdocOptions, LineWrappingStyle, OperatorPosition, QuoteProperties, QuoteStyle, Semicolons,
|
||||
SortImportsOptions, SortOrder, TrailingCommas,
|
||||
};
|
||||
use oxc_formatter_core::{CoreFormatOptions, FormatOptions};
|
||||
|
||||
@@ -13,8 +13,8 @@ use super::super::oxfmtrc::SortTailwindcssUserConfig;
|
||||
use super::super::oxfmtrc::{
|
||||
ArrowParensConfig, CommentLineStrategyConfig, FormatConfig, HtmlWhitespaceSensitivityConfig,
|
||||
ImportModifierConfig, ImportSelectorConfig, JsdocUserConfig, LineWrappingStyleConfig,
|
||||
ObjectWrapConfig, QuotePropsConfig, SortGroupItemConfig, SortImportsUserConfig,
|
||||
SortOrderConfig, TrailingCommaConfig,
|
||||
ObjectWrapConfig, OperatorPositionConfig, QuotePropsConfig, SortGroupItemConfig,
|
||||
SortImportsUserConfig, SortOrderConfig, TrailingCommaConfig,
|
||||
};
|
||||
|
||||
/// Convert `FormatConfig` into `JsFormatOptions` for `oxc_formatter`.
|
||||
@@ -29,10 +29,8 @@ pub fn to_oxc_formatter(
|
||||
let mut format_options = JsFormatOptions::default();
|
||||
format_options.apply_core(core_options);
|
||||
|
||||
// NOTE: Not yet supported options:
|
||||
// [Prettier] experimentalOperatorPosition: "start" | "end"
|
||||
// [Prettier] experimentalTernaries: boolean
|
||||
// These are rejected at deserialize time so they never reach here.
|
||||
// NOTE: [Prettier] experimentalTernaries is not yet supported;
|
||||
// rejected at deserialize time (`oxfmtrc::reject_experimental_ternaries`) so it never reaches here.
|
||||
|
||||
// [Prettier] singleQuote: boolean
|
||||
if let Some(single_quote) = config.single_quote {
|
||||
@@ -104,6 +102,14 @@ pub fn to_oxc_formatter(
|
||||
};
|
||||
}
|
||||
|
||||
// [Prettier] experimentalOperatorPosition: "start" | "end"
|
||||
if let Some(position) = config.experimental_operator_position {
|
||||
format_options.operator_position = match position {
|
||||
OperatorPositionConfig::Start => OperatorPosition::Start,
|
||||
OperatorPositionConfig::End => OperatorPosition::End,
|
||||
};
|
||||
}
|
||||
|
||||
// [Prettier] htmlWhitespaceSensitivity: "css" | "strict" | "ignore"
|
||||
if let Some(sensitivity) = config.html_whitespace_sensitivity {
|
||||
format_options.html_whitespace_sensitivity_ignore =
|
||||
|
||||
@@ -8,8 +8,9 @@ use oxc_formatter_core::LineWidth;
|
||||
|
||||
use super::super::oxfmtrc::{
|
||||
ArrowParensConfig, EmbeddedLanguageFormattingConfig, EndOfLineConfig, FormatConfig,
|
||||
HtmlWhitespaceSensitivityConfig, ObjectWrapConfig, ProseWrapConfig, QuotePropsConfig,
|
||||
SortTailwindcssUserConfig, SvelteConfig, SvelteUserConfig, TrailingCommaConfig,
|
||||
HtmlWhitespaceSensitivityConfig, ObjectWrapConfig, OperatorPositionConfig, ProseWrapConfig,
|
||||
QuotePropsConfig, SortTailwindcssUserConfig, SvelteConfig, SvelteUserConfig,
|
||||
TrailingCommaConfig,
|
||||
};
|
||||
|
||||
/// Build base Prettier-compatible options from a typed `FormatConfig`.
|
||||
@@ -105,6 +106,15 @@ pub fn to_prettier(config: &FormatConfig) -> Value {
|
||||
if let Some(v) = config.single_attribute_per_line {
|
||||
obj.insert("singleAttributePerLine".to_string(), Value::from(v));
|
||||
}
|
||||
if let Some(v) = config.experimental_operator_position {
|
||||
obj.insert(
|
||||
"experimentalOperatorPosition".to_string(),
|
||||
Value::from(match v {
|
||||
OperatorPositionConfig::Start => "start",
|
||||
OperatorPositionConfig::End => "end",
|
||||
}),
|
||||
);
|
||||
}
|
||||
if let Some(v) = config.embedded_language_formatting {
|
||||
obj.insert(
|
||||
"embeddedLanguageFormatting".to_string(),
|
||||
@@ -376,6 +386,7 @@ mod tests_to_prettier {
|
||||
"arrowParens": "avoid",
|
||||
"quoteProps": "consistent",
|
||||
"objectWrap": "collapse",
|
||||
"experimentalOperatorPosition": "start",
|
||||
"embeddedLanguageFormatting": "off",
|
||||
"proseWrap": "always",
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
@@ -391,6 +402,7 @@ mod tests_to_prettier {
|
||||
assert_eq!(obj.get("arrowParens"), Some(&Value::from("avoid")));
|
||||
assert_eq!(obj.get("quoteProps"), Some(&Value::from("consistent")));
|
||||
assert_eq!(obj.get("objectWrap"), Some(&Value::from("collapse")));
|
||||
assert_eq!(obj.get("experimentalOperatorPosition"), Some(&Value::from("start")));
|
||||
assert_eq!(obj.get("embeddedLanguageFormatting"), Some(&Value::from("off")));
|
||||
assert_eq!(obj.get("proseWrap"), Some(&Value::from("always")));
|
||||
assert_eq!(obj.get("htmlWhitespaceSensitivity"), Some(&Value::from("ignore")));
|
||||
@@ -423,7 +435,6 @@ mod tests_to_prettier {
|
||||
"jsdoc",
|
||||
"overrides",
|
||||
"ignorePatterns",
|
||||
"experimentalOperatorPosition",
|
||||
"experimentalTernaries",
|
||||
] {
|
||||
assert!(!obj.contains_key(key), "Key `{key}` must NOT be in Prettier options");
|
||||
|
||||
@@ -159,16 +159,16 @@ pub struct FormatConfig {
|
||||
/// - Default: `false`
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub single_attribute_per_line: Option<bool>,
|
||||
/// When expressions wrap lines, print operators at the start of new lines (`"start"`)
|
||||
/// or at the end of previous lines (`"end"`).
|
||||
///
|
||||
/// - Languages: JS, JSX, TS, TSX
|
||||
/// - Default: `"end"`
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub experimental_operator_position: Option<OperatorPositionConfig>,
|
||||
|
||||
// NOTE: These experimental options are not yet supported.
|
||||
// NOTE: This experimental option is not yet supported.
|
||||
// Reject at deserialize time so all entry paths (base / overrides / NAPI `resolve()`) are covered uniformly.
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "reject_experimental_operator_position",
|
||||
default
|
||||
)]
|
||||
#[schemars(skip)]
|
||||
pub experimental_operator_position: Option<String>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "reject_experimental_ternaries",
|
||||
@@ -354,17 +354,6 @@ impl FormatConfig {
|
||||
|
||||
// ---
|
||||
|
||||
fn reject_experimental_operator_position<'de, D>(d: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let v = Option::<String>::deserialize(d)?;
|
||||
if v.is_some() {
|
||||
return Err(serde::de::Error::custom("Unsupported option: `experimentalOperatorPosition`"));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn reject_experimental_ternaries<'de, D>(d: D) -> Result<Option<bool>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
@@ -416,6 +405,13 @@ pub enum ObjectWrapConfig {
|
||||
Collapse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OperatorPositionConfig {
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum EmbeddedLanguageFormattingConfig {
|
||||
@@ -1007,13 +1003,6 @@ mod tests_json_deep_merge {
|
||||
mod tests_reject_experimental {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_reject_experimental_operator_position_in_base() {
|
||||
let json = r#"{ "experimentalOperatorPosition": "start" }"#;
|
||||
let err = serde_json::from_str::<FormatConfig>(json).unwrap_err();
|
||||
assert!(err.to_string().contains("experimentalOperatorPosition"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reject_experimental_ternaries_in_base() {
|
||||
let json = r#"{ "experimentalTernaries": true }"#;
|
||||
@@ -1024,17 +1013,6 @@ mod tests_reject_experimental {
|
||||
#[test]
|
||||
fn test_reject_experimental_in_overrides() {
|
||||
// `OxfmtOverrideConfig.options: FormatConfig` so the same deserialize_with applies
|
||||
let json = r#"{
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"options": { "experimentalOperatorPosition": "end" }
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
let err = serde_json::from_str::<Oxfmtrc>(json).unwrap_err();
|
||||
assert!(err.to_string().contains("experimentalOperatorPosition"));
|
||||
|
||||
let json = r#"{
|
||||
"overrides": [
|
||||
{
|
||||
@@ -1046,23 +1024,4 @@ mod tests_reject_experimental {
|
||||
let err = serde_json::from_str::<Oxfmtrc>(json).unwrap_err();
|
||||
assert!(err.to_string().contains("experimentalTernaries"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reject_experimental_via_napi_resolve_path() {
|
||||
// NAPI `resolve()` does `serde_json::from_value::<FormatConfig>(raw_config)`,
|
||||
// which goes through the same deserialize_with.
|
||||
let raw = serde_json::json!({ "experimentalTernaries": true });
|
||||
let err = serde_json::from_value::<FormatConfig>(raw).unwrap_err();
|
||||
assert!(err.to_string().contains("experimentalTernaries"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unset_experimental_does_not_fail() {
|
||||
// Sanity: omitting both fields parses cleanly
|
||||
let json = r#"{ "printWidth": 120 }"#;
|
||||
let config: FormatConfig = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(config.print_width, Some(120));
|
||||
assert!(config.experimental_operator_position.is_none());
|
||||
assert!(config.experimental_ternaries.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -108,6 +108,8 @@ export interface OxcFormatterOptions {
|
||||
objectWrap?: string
|
||||
/** Put each attribute on its own line (default: false) */
|
||||
singleAttributePerLine?: boolean
|
||||
/** Where to print operators when binary expressions wrap lines: "start" | "end" (default: "end") */
|
||||
experimentalOperatorPosition?: string
|
||||
/** Sort imports configuration (default: None) */
|
||||
sortImports?: OxcSortImportsOptions
|
||||
}
|
||||
|
||||
@@ -108,6 +108,8 @@ export interface OxcFormatterOptions {
|
||||
objectWrap?: string
|
||||
/** Put each attribute on its own line (default: false) */
|
||||
singleAttributePerLine?: boolean
|
||||
/** Where to print operators when binary expressions wrap lines: "start" | "end" (default: "end") */
|
||||
experimentalOperatorPosition?: string
|
||||
/** Sort imports configuration (default: None) */
|
||||
sortImports?: OxcSortImportsOptions
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ use oxc::{
|
||||
};
|
||||
use oxc_formatter::{
|
||||
ArrowParentheses, AttributePosition, BracketSameLine, BracketSpacing, CustomGroupDefinition,
|
||||
Expand, GroupEntry, ImportModifier, ImportSelector, JsFormatOptions, QuoteProperties,
|
||||
QuoteStyle, Semicolons, SortImportsOptions, SortOrder, TrailingCommas, default_groups,
|
||||
default_internal_patterns,
|
||||
Expand, GroupEntry, ImportModifier, ImportSelector, JsFormatOptions, OperatorPosition,
|
||||
QuoteProperties, QuoteStyle, Semicolons, SortImportsOptions, SortOrder, TrailingCommas,
|
||||
default_groups, default_internal_patterns,
|
||||
};
|
||||
use oxc_formatter_core::{IndentStyle, IndentWidth, LineEnding, LineWidth};
|
||||
use oxc_linter::{
|
||||
@@ -517,6 +517,12 @@ impl Oxc {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref operator_position) = options.experimental_operator_position
|
||||
&& let Ok(position) = operator_position.parse::<OperatorPosition>()
|
||||
{
|
||||
format_options.operator_position = position;
|
||||
}
|
||||
|
||||
if let Some(ref sort_imports_config) = options.sort_imports {
|
||||
let order = sort_imports_config
|
||||
.order
|
||||
|
||||
@@ -150,6 +150,8 @@ pub struct OxcFormatterOptions {
|
||||
pub object_wrap: Option<String>,
|
||||
/// Put each attribute on its own line (default: false)
|
||||
pub single_attribute_per_line: Option<bool>,
|
||||
/// Where to print operators when binary expressions wrap lines: "start" | "end" (default: "end")
|
||||
pub experimental_operator_position: Option<String>,
|
||||
/// Sort imports configuration (default: None)
|
||||
pub sort_imports: Option<OxcSortImportsOptions>,
|
||||
}
|
||||
|
||||
@@ -41,6 +41,15 @@
|
||||
],
|
||||
"markdownDescription": "Which end of line characters to apply.\n\nNOTE: `\"auto\"` is not supported.\n\n- Languages: All\n- Default: `\"lf\"`\n- Overrides `.editorconfig.end_of_line`"
|
||||
},
|
||||
"experimentalOperatorPosition": {
|
||||
"description": "When expressions wrap lines, print operators at the start of new lines (`\"start\"`)\nor at the end of previous lines (`\"end\"`).\n\n- Languages: JS, JSX, TS, TSX\n- Default: `\"end\"`",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/OperatorPositionConfig"
|
||||
}
|
||||
],
|
||||
"markdownDescription": "When expressions wrap lines, print operators at the start of new lines (`\"start\"`)\nor at the end of previous lines (`\"end\"`).\n\n- Languages: JS, JSX, TS, TSX\n- Default: `\"end\"`"
|
||||
},
|
||||
"htmlWhitespaceSensitivity": {
|
||||
"description": "Specify the global whitespace sensitivity for HTML, Vue, Angular, and Handlebars.\n\n- Languages: HTML, Angular, Vue, Handlebars, Svelte\n- Default: `\"css\"`",
|
||||
"allOf": [
|
||||
@@ -307,6 +316,15 @@
|
||||
],
|
||||
"markdownDescription": "Which end of line characters to apply.\n\nNOTE: `\"auto\"` is not supported.\n\n- Languages: All\n- Default: `\"lf\"`\n- Overrides `.editorconfig.end_of_line`"
|
||||
},
|
||||
"experimentalOperatorPosition": {
|
||||
"description": "When expressions wrap lines, print operators at the start of new lines (`\"start\"`)\nor at the end of previous lines (`\"end\"`).\n\n- Languages: JS, JSX, TS, TSX\n- Default: `\"end\"`",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/OperatorPositionConfig"
|
||||
}
|
||||
],
|
||||
"markdownDescription": "When expressions wrap lines, print operators at the start of new lines (`\"start\"`)\nor at the end of previous lines (`\"end\"`).\n\n- Languages: JS, JSX, TS, TSX\n- Default: `\"end\"`"
|
||||
},
|
||||
"htmlWhitespaceSensitivity": {
|
||||
"description": "Specify the global whitespace sensitivity for HTML, Vue, Angular, and Handlebars.\n\n- Languages: HTML, Angular, Vue, Handlebars, Svelte\n- Default: `\"css\"`",
|
||||
"allOf": [
|
||||
@@ -601,6 +619,13 @@
|
||||
"collapse"
|
||||
]
|
||||
},
|
||||
"OperatorPositionConfig": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"start",
|
||||
"end"
|
||||
]
|
||||
},
|
||||
"OxfmtOverrideConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -71,6 +71,18 @@ NOTE: `"auto"` is not supported.
|
||||
- Overrides `.editorconfig.end_of_line`
|
||||
|
||||
|
||||
## experimentalOperatorPosition
|
||||
|
||||
type: `"start" | "end"`
|
||||
|
||||
|
||||
When expressions wrap lines, print operators at the start of new lines (`"start"`)
|
||||
or at the end of previous lines (`"end"`).
|
||||
|
||||
- Languages: JS, JSX, TS, TSX
|
||||
- Default: `"end"`
|
||||
|
||||
|
||||
## htmlWhitespaceSensitivity
|
||||
|
||||
type: `"css" | "strict" | "ignore"`
|
||||
|
||||
Reference in New Issue
Block a user