Skip to content

Commit 53ccefa

Browse files
committed
feat: add unionAndIntersectionType.operatorPosition option
Resolves with the same precedence as the other per-node operator-position overrides: node value > global `operatorPosition` > `nextLine`. Default behavior is unchanged. `sameLine` places `|` / `&` at the end of the wrapped line; `maintain` preserves the author's position (and falls back to dprint's default when the source is single-line, matching how `conditionalExpression.operatorPosition` handles `maintain`).
1 parent f7dd1f3 commit 53ccefa

10 files changed

Lines changed: 271 additions & 4 deletions

deployment/schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,9 @@
11991199
"conditionalType.operatorPosition": {
12001200
"$ref": "#/definitions/operatorPosition"
12011201
},
1202+
"unionAndIntersectionType.operatorPosition": {
1203+
"$ref": "#/definitions/operatorPosition"
1204+
},
12021205
"arguments.preferHanging": {
12031206
"$ref": "#/definitions/preferHangingGranular"
12041207
},

src/configuration/builder.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ impl ConfigurationBuilder {
5454
.binary_expression_operator_position(OperatorPosition::SameLine)
5555
.conditional_expression_operator_position(OperatorPosition::NextLine)
5656
.conditional_type_operator_position(OperatorPosition::NextLine)
57+
.union_and_intersection_type_operator_position(OperatorPosition::NextLine)
5758
.brace_position(BracePosition::SameLine)
5859
.comment_line_force_space_after_slashes(false)
5960
.construct_signature_space_after_new_keyword(true)
@@ -834,6 +835,10 @@ impl ConfigurationBuilder {
834835
self.insert("conditionalType.operatorPosition", value.to_string().into())
835836
}
836837

838+
pub fn union_and_intersection_type_operator_position(&mut self, value: OperatorPosition) -> &mut Self {
839+
self.insert("unionAndIntersectionType.operatorPosition", value.to_string().into())
840+
}
841+
837842
/* single body position */
838843

839844
pub fn if_statement_single_body_position(&mut self, value: SameOrNextLinePosition) -> &mut Self {
@@ -1202,6 +1207,7 @@ mod tests {
12021207
.binary_expression_operator_position(OperatorPosition::SameLine)
12031208
.conditional_expression_operator_position(OperatorPosition::SameLine)
12041209
.conditional_type_operator_position(OperatorPosition::SameLine)
1210+
.union_and_intersection_type_operator_position(OperatorPosition::SameLine)
12051211
/* single body position */
12061212
.if_statement_single_body_position(SameOrNextLinePosition::SameLine)
12071213
.for_statement_single_body_position(SameOrNextLinePosition::SameLine)
@@ -1305,7 +1311,7 @@ mod tests {
13051311
.while_statement_space_around(true);
13061312

13071313
let inner_config = config.get_inner_config();
1308-
assert_eq!(inner_config.len(), 182);
1314+
assert_eq!(inner_config.len(), 183);
13091315
let diagnostics = resolve_config(inner_config, &Default::default()).diagnostics;
13101316
assert_eq!(diagnostics.len(), 0);
13111317
}

src/configuration/resolve_config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
203203
binary_expression_operator_position: get_value(&mut config, "binaryExpression.operatorPosition", operator_position, &mut diagnostics),
204204
conditional_expression_operator_position: get_value(&mut config, "conditionalExpression.operatorPosition", operator_position, &mut diagnostics),
205205
conditional_type_operator_position: get_value(&mut config, "conditionalType.operatorPosition", operator_position, &mut diagnostics),
206+
union_and_intersection_type_operator_position: get_value(
207+
&mut config,
208+
"unionAndIntersectionType.operatorPosition",
209+
operator_position,
210+
&mut diagnostics,
211+
),
206212
/* single body position */
207213
if_statement_single_body_position: get_value(&mut config, "ifStatement.singleBodyPosition", single_body_position, &mut diagnostics),
208214
for_statement_single_body_position: get_value(&mut config, "forStatement.singleBodyPosition", single_body_position, &mut diagnostics),

src/configuration/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,8 @@ pub struct Configuration {
468468
pub conditional_expression_operator_position: OperatorPosition,
469469
#[serde(rename = "conditionalType.operatorPosition")]
470470
pub conditional_type_operator_position: OperatorPosition,
471+
#[serde(rename = "unionAndIntersectionType.operatorPosition")]
472+
pub union_and_intersection_type_operator_position: OperatorPosition,
471473
/* single body position */
472474
#[serde(rename = "ifStatement.singleBodyPosition")]
473475
pub if_statement_single_body_position: SameOrNextLinePosition,

src/generation/generate.rs

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6517,13 +6517,14 @@ struct UnionOrIntersectionType<'a, 'b> {
65176517
}
65186518

65196519
fn gen_union_or_intersection_type<'a, 'b>(node: UnionOrIntersectionType<'a, 'b>, context: &mut Context<'a>) -> PrintItems {
6520-
// todo: configuration for operator position
65216520
let mut items = PrintItems::new();
65226521
let force_use_new_lines = get_use_new_lines_for_nodes(node.types, context.config.union_and_intersection_type_prefer_single_line, context);
65236522
let separator = if node.is_union { sc!("|") } else { sc!("&") };
6523+
let trailing_separator = if node.is_union { sc!(" |") } else { sc!(" &") };
65246524

65256525
let indent_width = context.config.indent_width;
65266526
let prefer_hanging = context.config.union_and_intersection_type_prefer_hanging;
6527+
let operator_position = context.config.union_and_intersection_type_operator_position;
65276528
let is_parent_union_or_intersection = matches!(node.node.parent().unwrap().kind(), NodeKind::TsUnionType | NodeKind::TsIntersectionType);
65286529
let multi_line_options = if !is_parent_union_or_intersection {
65296530
if use_surround_newlines(node.node, context) {
@@ -6538,6 +6539,42 @@ fn gen_union_or_intersection_type<'a, 'b>(node: UnionOrIntersectionType<'a, 'b>,
65386539
|is_multi_line_or_hanging_ref| {
65396540
let is_multi_line_or_hanging = is_multi_line_or_hanging_ref.create_resolver();
65406541
let types_count = node.types.len();
6542+
6543+
// For each pair (between types[i-1] and types[i], i >= 1), decide whether the
6544+
// separator should be at the end of the previous line (SameLine) or at the
6545+
// start of the next line (NextLine). Index 0 is unused.
6546+
let pair_positions: Vec<OperatorPosition> = node
6547+
.types
6548+
.iter()
6549+
.enumerate()
6550+
.map(|(i, type_node)| {
6551+
if i == 0 {
6552+
operator_position
6553+
} else {
6554+
resolve_pair_position(&node, i, type_node, operator_position, separator.text, context)
6555+
}
6556+
})
6557+
.collect();
6558+
6559+
// Whether to emit the conditional leading separator on the first value when
6560+
// wrapping. NextLine = always (current behavior, leading-`|` hanging style).
6561+
// SameLine = never. Maintain = follow the source's leading-`|` presence.
6562+
let leading_first_when_multi_line = match operator_position {
6563+
OperatorPosition::NextLine => true,
6564+
OperatorPosition::SameLine => false,
6565+
OperatorPosition::Maintain => {
6566+
if node.node.start_line_fast(context.program) == node.node.end_line_fast(context.program) {
6567+
true
6568+
} else {
6569+
node
6570+
.types
6571+
.first()
6572+
.and_then(|t| context.token_finder.get_previous_token_if_operator(&t.range(), separator.text))
6573+
.is_some()
6574+
}
6575+
}
6576+
};
6577+
65416578
let mut generated_nodes = Vec::new();
65426579
for (i, type_node) in node.types.iter().enumerate() {
65436580
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>,
65526589
if let Some(separator_token) = separator_token {
65536590
items.extend(gen_leading_comments(&separator_token.range(), context));
65546591
}
6555-
if i == 0 && !is_parent_union_or_intersection {
6592+
6593+
let emit_leading_separator = i > 0 && matches!(pair_positions[i], OperatorPosition::NextLine);
6594+
if i == 0 && !is_parent_union_or_intersection && leading_first_when_multi_line {
65566595
items.push_condition(if_true("separatorIfMultiLine", is_multi_line_or_hanging.clone(), {
65576596
// todo: .into() implementation for StringContainer
65586597
let mut items = PrintItems::new();
65596598
items.push_sc(separator);
65606599
items
65616600
}));
6562-
} else if i > 0 {
6601+
} else if emit_leading_separator {
65636602
items.push_sc(separator);
65646603
}
65656604

@@ -6579,6 +6618,11 @@ fn gen_union_or_intersection_type<'a, 'b>(node: UnionOrIntersectionType<'a, 'b>,
65796618
));
65806619
items.extend(gen_node(type_node.into(), context));
65816620

6621+
let next_is_same_line = i + 1 < types_count && matches!(pair_positions[i + 1], OperatorPosition::SameLine);
6622+
if next_is_same_line {
6623+
items.push_sc(trailing_separator);
6624+
}
6625+
65826626
generated_nodes.push(ir_helpers::GeneratedValue {
65836627
items,
65846628
lines_span: None,
@@ -6612,6 +6656,39 @@ fn gen_union_or_intersection_type<'a, 'b>(node: UnionOrIntersectionType<'a, 'b>,
66126656
_ => false,
66136657
}
66146658
}
6659+
6660+
fn resolve_pair_position<'a, 'b>(
6661+
node: &UnionOrIntersectionType<'a, 'b>,
6662+
i: usize,
6663+
type_node: &TsType<'a>,
6664+
operator_position: OperatorPosition,
6665+
separator_text: &str,
6666+
context: &mut Context<'a>,
6667+
) -> OperatorPosition {
6668+
match operator_position {
6669+
OperatorPosition::NextLine => OperatorPosition::NextLine,
6670+
OperatorPosition::SameLine => OperatorPosition::SameLine,
6671+
OperatorPosition::Maintain => {
6672+
// When the whole union/intersection is on one source line, prefer dprint's
6673+
// default (NextLine) for the wrapping layout — matches how `Maintain` is
6674+
// handled for conditional expressions/types.
6675+
if node.node.start_line_fast(context.program) == node.node.end_line_fast(context.program) {
6676+
return OperatorPosition::NextLine;
6677+
}
6678+
match context.token_finder.get_previous_token_if_operator(&type_node.range(), separator_text) {
6679+
Some(sep_token) => {
6680+
let prev_end_line = node.types[i - 1].end_line_fast(context.program);
6681+
if prev_end_line == sep_token.start_line_fast(context.program) {
6682+
OperatorPosition::SameLine
6683+
} else {
6684+
OperatorPosition::NextLine
6685+
}
6686+
}
6687+
None => OperatorPosition::NextLine,
6688+
}
6689+
}
6690+
}
6691+
}
66156692
}
66166693

66176694
/* comments */
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
~~ lineWidth: 40, unionAndIntersectionType.operatorPosition: maintain ~~
2+
== should use dprint default (leading) when source is single line ==
3+
export type T = string & test & string & test;
4+
5+
[expect]
6+
export type T =
7+
& string
8+
& test
9+
& string
10+
& test;
11+
12+
== should preserve trailing `&` when source uses trailing style ==
13+
export type T =
14+
string &
15+
test &
16+
other;
17+
18+
[expect]
19+
export type T =
20+
string &
21+
test &
22+
other;
23+
24+
== should preserve leading `&` when source uses leading style ==
25+
export type T =
26+
& string
27+
& test
28+
& other;
29+
30+
[expect]
31+
export type T =
32+
& string
33+
& test
34+
& other;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
~~ lineWidth: 40, unionAndIntersectionType.operatorPosition: sameLine ~~
2+
== should place `&` at end of line when wrapping ==
3+
export type T = string & test & string & test;
4+
5+
[expect]
6+
export type T =
7+
string &
8+
test &
9+
string &
10+
test;
11+
12+
== should keep single line when it fits ==
13+
export type T = string & number;
14+
15+
[expect]
16+
export type T = string & number;
17+
18+
== should rewrite leading `&` style to trailing ==
19+
export type T =
20+
& string
21+
& test
22+
& other;
23+
24+
[expect]
25+
export type T =
26+
string &
27+
test &
28+
other;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
~~ lineWidth: 40, operatorPosition: sameLine ~~
2+
== should fall back to global operatorPosition when union override is unset ==
3+
export type T = string | test | string | number;
4+
5+
[expect]
6+
export type T =
7+
string |
8+
test |
9+
string |
10+
number;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
~~ lineWidth: 40, unionAndIntersectionType.operatorPosition: maintain ~~
2+
== should use dprint default (leading) when source is single line ==
3+
export type T = string | test | string | number;
4+
5+
[expect]
6+
export type T =
7+
| string
8+
| test
9+
| string
10+
| number;
11+
12+
== should preserve trailing operators when source uses trailing style ==
13+
export type T =
14+
string |
15+
test |
16+
other;
17+
18+
[expect]
19+
export type T =
20+
string |
21+
test |
22+
other;
23+
24+
== should preserve leading operators when source uses leading style ==
25+
export type T =
26+
| string
27+
| test
28+
| other;
29+
30+
[expect]
31+
export type T =
32+
| string
33+
| test
34+
| other;
35+
36+
== should preserve mixed positions ==
37+
export type T =
38+
string |
39+
test
40+
| other;
41+
42+
[expect]
43+
export type T =
44+
string |
45+
test
46+
| other;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
~~ lineWidth: 40, unionAndIntersectionType.operatorPosition: sameLine ~~
2+
== should format with operator at end of line when wrapping ==
3+
export type T = string | test | string | number;
4+
5+
[expect]
6+
export type T =
7+
string |
8+
test |
9+
string |
10+
number;
11+
12+
== should keep single line when it fits ==
13+
export type T = string | number;
14+
15+
[expect]
16+
export type T = string | number;
17+
18+
== should rewrite leading operator style to trailing ==
19+
export type T =
20+
| string
21+
| test
22+
| other;
23+
24+
[expect]
25+
export type T =
26+
string |
27+
test |
28+
other;
29+
30+
== should keep trailing operator style as-is ==
31+
export type T =
32+
string |
33+
test |
34+
other;
35+
36+
[expect]
37+
export type T =
38+
string |
39+
test |
40+
other;
41+
42+
== should produce trailing operators for the example from issue #759 ==
43+
export type T = (
44+
"option_a" |
45+
"option_b" |
46+
"option_c" |
47+
"option_d"
48+
);
49+
50+
[expect]
51+
export type T =
52+
"option_a" |
53+
"option_b" |
54+
"option_c" |
55+
"option_d";

0 commit comments

Comments
 (0)