Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions deployment/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,9 @@
"conditionalType.operatorPosition": {
"$ref": "#/definitions/operatorPosition"
},
"unionAndIntersectionType.operatorPosition": {
"$ref": "#/definitions/operatorPosition"
},
"arguments.preferHanging": {
"$ref": "#/definitions/preferHangingGranular"
},
Expand Down
8 changes: 7 additions & 1 deletion src/configuration/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 6 additions & 0 deletions src/configuration/resolve_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions src/configuration/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
83 changes: 80 additions & 3 deletions src/generation/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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<OperatorPosition> = 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) = {
Expand All @@ -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);
}

Expand All @@ -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,
Expand Down Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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";