diff --git a/deployment/schema.json b/deployment/schema.json index 13e5b70d..f1818b9c 100644 --- a/deployment/schema.json +++ b/deployment/schema.json @@ -1199,6 +1199,9 @@ "conditionalType.operatorPosition": { "$ref": "#/definitions/operatorPosition" }, + "unionAndIntersectionType.operatorPosition": { + "$ref": "#/definitions/operatorPosition" + }, "arguments.preferHanging": { "$ref": "#/definitions/preferHangingGranular" }, diff --git a/src/configuration/builder.rs b/src/configuration/builder.rs index bfa35df5..625fae0b 100644 --- a/src/configuration/builder.rs +++ b/src/configuration/builder.rs @@ -54,6 +54,7 @@ impl ConfigurationBuilder { .binary_expression_operator_position(OperatorPosition::SameLine) .conditional_expression_operator_position(OperatorPosition::NextLine) .conditional_type_operator_position(OperatorPosition::NextLine) + .union_and_intersection_type_operator_position(OperatorPosition::NextLine) .brace_position(BracePosition::SameLine) .comment_line_force_space_after_slashes(false) .construct_signature_space_after_new_keyword(true) @@ -834,6 +835,10 @@ impl ConfigurationBuilder { self.insert("conditionalType.operatorPosition", value.to_string().into()) } + pub fn union_and_intersection_type_operator_position(&mut self, value: OperatorPosition) -> &mut Self { + self.insert("unionAndIntersectionType.operatorPosition", value.to_string().into()) + } + /* single body position */ pub fn if_statement_single_body_position(&mut self, value: SameOrNextLinePosition) -> &mut Self { @@ -1202,6 +1207,7 @@ mod tests { .binary_expression_operator_position(OperatorPosition::SameLine) .conditional_expression_operator_position(OperatorPosition::SameLine) .conditional_type_operator_position(OperatorPosition::SameLine) + .union_and_intersection_type_operator_position(OperatorPosition::SameLine) /* single body position */ .if_statement_single_body_position(SameOrNextLinePosition::SameLine) .for_statement_single_body_position(SameOrNextLinePosition::SameLine) @@ -1305,7 +1311,7 @@ mod tests { .while_statement_space_around(true); let inner_config = config.get_inner_config(); - assert_eq!(inner_config.len(), 182); + assert_eq!(inner_config.len(), 183); let diagnostics = resolve_config(inner_config, &Default::default()).diagnostics; assert_eq!(diagnostics.len(), 0); } diff --git a/src/configuration/resolve_config.rs b/src/configuration/resolve_config.rs index ea393be3..f00bfbda 100644 --- a/src/configuration/resolve_config.rs +++ b/src/configuration/resolve_config.rs @@ -203,6 +203,12 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration) binary_expression_operator_position: get_value(&mut config, "binaryExpression.operatorPosition", operator_position, &mut diagnostics), conditional_expression_operator_position: get_value(&mut config, "conditionalExpression.operatorPosition", operator_position, &mut diagnostics), conditional_type_operator_position: get_value(&mut config, "conditionalType.operatorPosition", operator_position, &mut diagnostics), + union_and_intersection_type_operator_position: get_value( + &mut config, + "unionAndIntersectionType.operatorPosition", + operator_position, + &mut diagnostics, + ), /* single body position */ if_statement_single_body_position: get_value(&mut config, "ifStatement.singleBodyPosition", single_body_position, &mut diagnostics), for_statement_single_body_position: get_value(&mut config, "forStatement.singleBodyPosition", single_body_position, &mut diagnostics), diff --git a/src/configuration/types.rs b/src/configuration/types.rs index 61b08bf7..53ed6a58 100644 --- a/src/configuration/types.rs +++ b/src/configuration/types.rs @@ -468,6 +468,8 @@ pub struct Configuration { pub conditional_expression_operator_position: OperatorPosition, #[serde(rename = "conditionalType.operatorPosition")] pub conditional_type_operator_position: OperatorPosition, + #[serde(rename = "unionAndIntersectionType.operatorPosition")] + pub union_and_intersection_type_operator_position: OperatorPosition, /* single body position */ #[serde(rename = "ifStatement.singleBodyPosition")] pub if_statement_single_body_position: SameOrNextLinePosition, diff --git a/src/generation/generate.rs b/src/generation/generate.rs index 7fb3a332..79000ac8 100644 --- a/src/generation/generate.rs +++ b/src/generation/generate.rs @@ -6517,13 +6517,14 @@ struct UnionOrIntersectionType<'a, 'b> { } fn gen_union_or_intersection_type<'a, 'b>(node: UnionOrIntersectionType<'a, 'b>, context: &mut Context<'a>) -> PrintItems { - // todo: configuration for operator position let mut items = PrintItems::new(); let force_use_new_lines = get_use_new_lines_for_nodes(node.types, context.config.union_and_intersection_type_prefer_single_line, context); let separator = if node.is_union { sc!("|") } else { sc!("&") }; + let trailing_separator = if node.is_union { sc!(" |") } else { sc!(" &") }; let indent_width = context.config.indent_width; let prefer_hanging = context.config.union_and_intersection_type_prefer_hanging; + let operator_position = context.config.union_and_intersection_type_operator_position; let is_parent_union_or_intersection = matches!(node.node.parent().unwrap().kind(), NodeKind::TsUnionType | NodeKind::TsIntersectionType); let multi_line_options = if !is_parent_union_or_intersection { if use_surround_newlines(node.node, context) { @@ -6538,6 +6539,42 @@ fn gen_union_or_intersection_type<'a, 'b>(node: UnionOrIntersectionType<'a, 'b>, |is_multi_line_or_hanging_ref| { let is_multi_line_or_hanging = is_multi_line_or_hanging_ref.create_resolver(); let types_count = node.types.len(); + + // For each pair (between types[i-1] and types[i], i >= 1), decide whether the + // separator should be at the end of the previous line (SameLine) or at the + // start of the next line (NextLine). Index 0 is unused. + let pair_positions: Vec = node + .types + .iter() + .enumerate() + .map(|(i, type_node)| { + if i == 0 { + operator_position + } else { + resolve_pair_position(&node, i, type_node, operator_position, separator.text, context) + } + }) + .collect(); + + // Whether to emit the conditional leading separator on the first value when + // wrapping. NextLine = always (current behavior, leading-`|` hanging style). + // SameLine = never. Maintain = follow the source's leading-`|` presence. + let leading_first_when_multi_line = match operator_position { + OperatorPosition::NextLine => true, + OperatorPosition::SameLine => false, + OperatorPosition::Maintain => { + if node.node.start_line_fast(context.program) == node.node.end_line_fast(context.program) { + true + } else { + node + .types + .first() + .and_then(|t| context.token_finder.get_previous_token_if_operator(&t.range(), separator.text)) + .is_some() + } + } + }; + let mut generated_nodes = Vec::new(); for (i, type_node) in node.types.iter().enumerate() { let (allow_inline_multi_line, allow_inline_single_line) = { @@ -6552,14 +6589,16 @@ fn gen_union_or_intersection_type<'a, 'b>(node: UnionOrIntersectionType<'a, 'b>, if let Some(separator_token) = separator_token { items.extend(gen_leading_comments(&separator_token.range(), context)); } - if i == 0 && !is_parent_union_or_intersection { + + let emit_leading_separator = i > 0 && matches!(pair_positions[i], OperatorPosition::NextLine); + if i == 0 && !is_parent_union_or_intersection && leading_first_when_multi_line { items.push_condition(if_true("separatorIfMultiLine", is_multi_line_or_hanging.clone(), { // todo: .into() implementation for StringContainer let mut items = PrintItems::new(); items.push_sc(separator); items })); - } else if i > 0 { + } else if emit_leading_separator { items.push_sc(separator); } @@ -6579,6 +6618,11 @@ fn gen_union_or_intersection_type<'a, 'b>(node: UnionOrIntersectionType<'a, 'b>, )); items.extend(gen_node(type_node.into(), context)); + let next_is_same_line = i + 1 < types_count && matches!(pair_positions[i + 1], OperatorPosition::SameLine); + if next_is_same_line { + items.push_sc(trailing_separator); + } + generated_nodes.push(ir_helpers::GeneratedValue { items, lines_span: None, @@ -6612,6 +6656,39 @@ fn gen_union_or_intersection_type<'a, 'b>(node: UnionOrIntersectionType<'a, 'b>, _ => false, } } + + fn resolve_pair_position<'a, 'b>( + node: &UnionOrIntersectionType<'a, 'b>, + i: usize, + type_node: &TsType<'a>, + operator_position: OperatorPosition, + separator_text: &str, + context: &mut Context<'a>, + ) -> OperatorPosition { + match operator_position { + OperatorPosition::NextLine => OperatorPosition::NextLine, + OperatorPosition::SameLine => OperatorPosition::SameLine, + OperatorPosition::Maintain => { + // When the whole union/intersection is on one source line, prefer dprint's + // default (NextLine) for the wrapping layout — matches how `Maintain` is + // handled for conditional expressions/types. + if node.node.start_line_fast(context.program) == node.node.end_line_fast(context.program) { + return OperatorPosition::NextLine; + } + match context.token_finder.get_previous_token_if_operator(&type_node.range(), separator_text) { + Some(sep_token) => { + let prev_end_line = node.types[i - 1].end_line_fast(context.program); + if prev_end_line == sep_token.start_line_fast(context.program) { + OperatorPosition::SameLine + } else { + OperatorPosition::NextLine + } + } + None => OperatorPosition::NextLine, + } + } + } + } } /* comments */ diff --git a/tests/specs/types/IntersectionType/IntersectionType_OperatorPosition_Maintain.txt b/tests/specs/types/IntersectionType/IntersectionType_OperatorPosition_Maintain.txt new file mode 100644 index 00000000..f3231cb3 --- /dev/null +++ b/tests/specs/types/IntersectionType/IntersectionType_OperatorPosition_Maintain.txt @@ -0,0 +1,34 @@ +~~ lineWidth: 40, unionAndIntersectionType.operatorPosition: maintain ~~ +== should use dprint default (leading) when source is single line == +export type T = string & test & string & test; + +[expect] +export type T = + & string + & test + & string + & test; + +== should preserve trailing `&` when source uses trailing style == +export type T = + string & + test & + other; + +[expect] +export type T = + string & + test & + other; + +== should preserve leading `&` when source uses leading style == +export type T = + & string + & test + & other; + +[expect] +export type T = + & string + & test + & other; diff --git a/tests/specs/types/IntersectionType/IntersectionType_OperatorPosition_SameLine.txt b/tests/specs/types/IntersectionType/IntersectionType_OperatorPosition_SameLine.txt new file mode 100644 index 00000000..d526fc5d --- /dev/null +++ b/tests/specs/types/IntersectionType/IntersectionType_OperatorPosition_SameLine.txt @@ -0,0 +1,28 @@ +~~ lineWidth: 40, unionAndIntersectionType.operatorPosition: sameLine ~~ +== should place `&` at end of line when wrapping == +export type T = string & test & string & test; + +[expect] +export type T = + string & + test & + string & + test; + +== should keep single line when it fits == +export type T = string & number; + +[expect] +export type T = string & number; + +== should rewrite leading `&` style to trailing == +export type T = + & string + & test + & other; + +[expect] +export type T = + string & + test & + other; diff --git a/tests/specs/types/UnionType/UnionType_OperatorPosition_FallbackGlobal.txt b/tests/specs/types/UnionType/UnionType_OperatorPosition_FallbackGlobal.txt new file mode 100644 index 00000000..ec359d0e --- /dev/null +++ b/tests/specs/types/UnionType/UnionType_OperatorPosition_FallbackGlobal.txt @@ -0,0 +1,10 @@ +~~ lineWidth: 40, operatorPosition: sameLine ~~ +== should fall back to global operatorPosition when union override is unset == +export type T = string | test | string | number; + +[expect] +export type T = + string | + test | + string | + number; diff --git a/tests/specs/types/UnionType/UnionType_OperatorPosition_Maintain.txt b/tests/specs/types/UnionType/UnionType_OperatorPosition_Maintain.txt new file mode 100644 index 00000000..60f025c5 --- /dev/null +++ b/tests/specs/types/UnionType/UnionType_OperatorPosition_Maintain.txt @@ -0,0 +1,46 @@ +~~ lineWidth: 40, unionAndIntersectionType.operatorPosition: maintain ~~ +== should use dprint default (leading) when source is single line == +export type T = string | test | string | number; + +[expect] +export type T = + | string + | test + | string + | number; + +== should preserve trailing operators when source uses trailing style == +export type T = + string | + test | + other; + +[expect] +export type T = + string | + test | + other; + +== should preserve leading operators when source uses leading style == +export type T = + | string + | test + | other; + +[expect] +export type T = + | string + | test + | other; + +== should preserve mixed positions == +export type T = + string | + test + | other; + +[expect] +export type T = + string | + test + | other; diff --git a/tests/specs/types/UnionType/UnionType_OperatorPosition_SameLine.txt b/tests/specs/types/UnionType/UnionType_OperatorPosition_SameLine.txt new file mode 100644 index 00000000..a99f0b52 --- /dev/null +++ b/tests/specs/types/UnionType/UnionType_OperatorPosition_SameLine.txt @@ -0,0 +1,55 @@ +~~ lineWidth: 40, unionAndIntersectionType.operatorPosition: sameLine ~~ +== should format with operator at end of line when wrapping == +export type T = string | test | string | number; + +[expect] +export type T = + string | + test | + string | + number; + +== should keep single line when it fits == +export type T = string | number; + +[expect] +export type T = string | number; + +== should rewrite leading operator style to trailing == +export type T = + | string + | test + | other; + +[expect] +export type T = + string | + test | + other; + +== should keep trailing operator style as-is == +export type T = + string | + test | + other; + +[expect] +export type T = + string | + test | + other; + +== should produce trailing operators for the example from issue #759 == +export type T = ( + "option_a" | + "option_b" | + "option_c" | + "option_d" +); + +[expect] +export type T = + "option_a" | + "option_b" | + "option_c" | + "option_d";