Skip to content

Commit d03e4de

Browse files
committed
feat(formatter): implement formatting for TSUnionType
1 parent 0f15ed3 commit d03e4de

File tree

7 files changed

+290
-52
lines changed

7 files changed

+290
-52
lines changed

crates/oxc_formatter/src/formatter/comments.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,27 @@ impl<'a> Comments<'a> {
173173
&self.unprinted_comments()[..index]
174174
}
175175

176+
/// Returns end-of-line comments that are after the given position (excluding printed ones).
177+
pub fn end_of_line_comments_after(&self, mut pos: u32) -> &'a [Comment] {
178+
let comments = self.unprinted_comments();
179+
for (index, comment) in comments.iter().enumerate() {
180+
if self
181+
.source_text
182+
.all_bytes_match(pos, comment.span.start, |b| matches!(b, b'\t' | b' ' | b')'))
183+
{
184+
if !self.source_text.is_own_line_comment(comment)
185+
&& (comment.is_line() || self.source_text.is_end_of_line_comment(comment))
186+
{
187+
return &comments[..=index];
188+
}
189+
pos = comment.span.end;
190+
} else {
191+
break;
192+
}
193+
}
194+
&[]
195+
}
196+
176197
/// Returns comments that start after the given position (excluding printed ones).
177198
pub fn comments_after(&self, pos: u32) -> &'a [Comment] {
178199
let comments = self.unprinted_comments();

crates/oxc_formatter/src/generated/format.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3786,9 +3786,19 @@ impl<'a> Format<'a> for AstNode<'a, TSConditionalType<'a>> {
37863786
impl<'a> Format<'a> for AstNode<'a, TSUnionType<'a>> {
37873787
fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
37883788
let is_suppressed = f.comments().is_suppressed(self.span().start);
3789+
if !is_suppressed && format_type_cast_comment_node(self, false, f)? {
3790+
return Ok(());
3791+
}
37893792
self.format_leading_comments(f)?;
3793+
let needs_parentheses = self.needs_parentheses(f);
3794+
if needs_parentheses {
3795+
"(".fmt(f)?;
3796+
}
37903797
let result =
37913798
if is_suppressed { FormatSuppressedNode(self.span()).fmt(f) } else { self.write(f) };
3799+
if needs_parentheses {
3800+
")".fmt(f)?;
3801+
}
37923802
self.format_trailing_comments(f)?;
37933803
result
37943804
}

crates/oxc_formatter/src/parentheses/ts_type.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,22 @@ impl<'a> NeedsParentheses<'a> for AstNode<'a, TSConstructorType<'a>> {
6060

6161
impl<'a> NeedsParentheses<'a> for AstNode<'a, TSUnionType<'a>> {
6262
fn needs_parentheses(&self, f: &Formatter<'_, 'a>) -> bool {
63-
matches!(
64-
self.parent,
65-
AstNodes::TSArrayType(_)
66-
| AstNodes::TSTypeOperator(_)
67-
| AstNodes::TSIndexedAccessType(_)
68-
)
63+
match self.parent {
64+
AstNodes::TSUnionType(union) => self.types.len() > 1 && union.types.len() > 1,
65+
AstNodes::TSIntersectionType(intersection) => {
66+
self.types.len() > 1 && intersection.types.len() > 1
67+
}
68+
parent => operator_type_or_higher_needs_parens(self.span(), parent),
69+
}
70+
}
71+
}
72+
73+
/// Returns `true` if a TS primary type needs parentheses
74+
fn operator_type_or_higher_needs_parens(span: Span, parent: &AstNodes) -> bool {
75+
match parent {
76+
AstNodes::TSArrayType(_) | AstNodes::TSTypeOperator(_) | AstNodes::TSRestType(_) => true,
77+
AstNodes::TSIndexedAccessType(indexed) => indexed.object_type.span() == span,
78+
_ => false,
6979
}
7080
}
7181

crates/oxc_formatter/src/write/mod.rs

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ mod switch_statement;
2626
mod template;
2727
mod try_statement;
2828
mod type_parameters;
29+
mod union_type;
2930
mod utils;
3031
mod variable_declaration;
3132

@@ -1209,26 +1210,6 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSConditionalType<'a>> {
12091210
}
12101211
}
12111212

1212-
impl<'a> FormatWrite<'a> for AstNode<'a, TSUnionType<'a>> {
1213-
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
1214-
let mut types = self.types().iter();
1215-
if self.needs_parentheses(f) {
1216-
write!(f, "(")?;
1217-
}
1218-
if let Some(item) = types.next() {
1219-
write!(f, item)?;
1220-
1221-
for item in types {
1222-
write!(f, [" | ", item])?;
1223-
}
1224-
}
1225-
if self.needs_parentheses(f) {
1226-
write!(f, ")")?;
1227-
}
1228-
Ok(())
1229-
}
1230-
}
1231-
12321213
impl<'a> FormatWrite<'a> for AstNode<'a, TSIntersectionType<'a>> {
12331214
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
12341215
let mut types = self.types().iter();
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
use oxc_allocator::Vec;
2+
use oxc_ast::ast::*;
3+
use oxc_span::GetSpan;
4+
5+
use crate::{
6+
format_args,
7+
formatter::{FormatResult, Formatter, prelude::*, trivia::FormatTrailingComments},
8+
generated::ast_nodes::{AstNode, AstNodes},
9+
parentheses::NeedsParentheses,
10+
write,
11+
write::FormatWrite,
12+
};
13+
14+
impl<'a> FormatWrite<'a> for AstNode<'a, TSUnionType<'a>> {
15+
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
16+
let types = self.types();
17+
18+
if types.len() == 1 {
19+
return write!(f, self.types().first());
20+
}
21+
22+
// ```ts
23+
// {
24+
// a: string
25+
// } | null | void
26+
// ```
27+
// should be inlined and not be printed in the multi-line variant
28+
let should_hug = should_hug_type(self);
29+
if should_hug {
30+
return format_union_types(self.types(), true, f);
31+
}
32+
33+
// Find the head of the nest union type chain
34+
// ```js
35+
// type Foo = | (| (A | B))
36+
// ^^^^^
37+
// ```
38+
// If the current union type is `A | B`
39+
// - `A | B` is the inner union type of `| (A | B)`
40+
// - `| (A | B)` is the inner union type of `| (| (A | B))`
41+
//
42+
// So the head of the current nested union type chain is `| (| (A | B))`
43+
// if we encounter a leading comment when navigating up the chain,
44+
// we consider the current union type as having leading comments
45+
let mut has_leading_comments = f.comments().has_comment_before(self.span().start);
46+
let mut union_type_at_top = self;
47+
48+
while let AstNodes::TSUnionType(parent) = union_type_at_top.parent {
49+
if parent.types().len() == 1 {
50+
if f.comments().has_comment_before(parent.span().start) {
51+
has_leading_comments = true;
52+
}
53+
union_type_at_top = parent;
54+
} else {
55+
break;
56+
}
57+
}
58+
59+
let should_indent = {
60+
let parent = union_type_at_top.parent;
61+
62+
// These parents have indent for their content, so we don't need to indent here
63+
!match parent {
64+
AstNodes::TSTypeAliasDeclaration(_) => has_leading_comments,
65+
AstNodes::TSTypeAssertion(_)
66+
| AstNodes::TSTupleType(_)
67+
| AstNodes::TSTypeParameterInstantiation(_) => true,
68+
_ => false,
69+
}
70+
};
71+
72+
let types = format_with(|f| {
73+
if has_leading_comments {
74+
write!(f, [soft_line_break()])?;
75+
}
76+
77+
let leading_soft_line_break_or_space = should_indent && !has_leading_comments;
78+
79+
let separator = format_with(|f| {
80+
if leading_soft_line_break_or_space {
81+
write!(f, [soft_line_break_or_space()])?;
82+
}
83+
write!(f, [text("|"), space()])
84+
});
85+
86+
write!(f, [if_group_breaks(&separator)])?;
87+
88+
format_union_types(types, false, f)
89+
});
90+
91+
let content = format_with(|f| {
92+
// it is necessary to add parentheses for unions in intersections
93+
// ```ts
94+
// type Some = B & (C | A) & D
95+
// ```
96+
if self.needs_parentheses(f) {
97+
return write!(f, [indent(&types), soft_line_break()]);
98+
}
99+
100+
let is_inside_complex_tuple_type = match self.parent {
101+
AstNodes::TSTupleType(tuple) => tuple.element_types().len() > 1,
102+
_ => false,
103+
};
104+
105+
if is_inside_complex_tuple_type {
106+
write!(
107+
f,
108+
[
109+
indent(&format_args!(
110+
if_group_breaks(&format_args!(text("("), soft_line_break())),
111+
types
112+
)),
113+
soft_line_break(),
114+
if_group_breaks(&text(")"))
115+
]
116+
)
117+
} else if should_indent {
118+
write!(f, [indent(&types)])
119+
} else {
120+
write!(f, [types])
121+
}
122+
});
123+
124+
write!(f, [group(&content)])
125+
}
126+
}
127+
128+
fn should_hug_type(node: &AstNode<'_, TSUnionType<'_>>) -> bool {
129+
// Simple heuristic: hug unions with object types and simple nullable types
130+
let types = node.types();
131+
132+
if types.len() <= 3 {
133+
let has_object_type = types.iter().any(|t| matches!(t.as_ref(), TSType::TSTypeLiteral(_)));
134+
135+
let has_simple_types = types.iter().any(|t| {
136+
matches!(
137+
t.as_ref(),
138+
TSType::TSNullKeyword(_) | TSType::TSUndefinedKeyword(_) | TSType::TSVoidKeyword(_)
139+
)
140+
});
141+
142+
return has_object_type && has_simple_types;
143+
}
144+
145+
false
146+
}
147+
148+
pub struct FormatTSType<'a, 'b> {
149+
next_node_span: Option<Span>,
150+
element: &'b AstNode<'a, TSType<'a>>,
151+
should_hug: bool,
152+
}
153+
154+
impl<'a> Format<'a> for FormatTSType<'a, '_> {
155+
fn fmt(&self, f: &mut crate::formatter::Formatter<'_, 'a>) -> FormatResult<()> {
156+
let format_element = format_once(|f| {
157+
self.element.fmt(f)?;
158+
Ok(())
159+
});
160+
if self.should_hug {
161+
write!(f, [format_element])?;
162+
} else {
163+
write!(f, [align(2, &format_element)])?;
164+
}
165+
166+
if let Some(next_node_span) = self.next_node_span {
167+
let comments_before_separator =
168+
f.context().comments().comments_before_character(self.element.span().end, b'|');
169+
FormatTrailingComments::Comments(comments_before_separator).fmt(f)?;
170+
171+
// ```ts
172+
// type Some = A |
173+
// // comment
174+
// B
175+
// ```
176+
// to
177+
// ```ts
178+
// type Some =
179+
// | A
180+
// // comment
181+
// | B
182+
// ```
183+
// If there is a leading own line comment between `|` and the next node, we need to put print comments
184+
// before `|` instead of after it.
185+
if f.comments().has_leading_own_line_comment(next_node_span.start) {
186+
let comments = f.context().comments().comments_before(next_node_span.start);
187+
FormatTrailingComments::Comments(comments).fmt(f)?;
188+
}
189+
190+
if self.should_hug {
191+
write!(f, [space()])?;
192+
} else {
193+
write!(f, [soft_line_break_or_space()])?;
194+
}
195+
write!(f, ["|"])
196+
} else {
197+
// ```ts
198+
// type Foo = (
199+
// | "thing1" // comment1
200+
// | "thing2" // comment2
201+
// ^^^^^^^^^^^ the following logic is to print comment2,
202+
// )[]; // comment 3
203+
//```
204+
// TODO: We may need to tweak `AstNode<'a, Vec<'a, T>>` iterator as some of Vec's last elements should have the following span.
205+
let comments =
206+
f.context().comments().end_of_line_comments_after(self.element.span().end);
207+
FormatTrailingComments::Comments(comments).fmt(f)
208+
}
209+
}
210+
}
211+
212+
fn format_union_types<'a>(
213+
node: &AstNode<'a, Vec<'a, TSType<'a>>>,
214+
should_hug: bool,
215+
f: &mut Formatter<'_, 'a>,
216+
) -> FormatResult<()> {
217+
f.join_with(space())
218+
.entries(node.iter().enumerate().map(|(index, item)| FormatTSType {
219+
next_node_span: node.get(index + 1).map(GetSpan::span),
220+
element: item,
221+
should_hug,
222+
}))
223+
.finish()
224+
}

tasks/ast_tools/src/generators/formatter/format.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const AST_NODE_WITHOUT_PRINTING_COMMENTS_LIST: &[&str] = &[
3838
];
3939

4040
const AST_NODE_NEEDS_PARENTHESES: &[&str] =
41-
&["TSTypeAssertion", "TSInferType", "TSConditionalType"];
41+
&["TSTypeAssertion", "TSInferType", "TSConditionalType", "TSUnionType"];
4242

4343
const NEEDS_IMPLEMENTING_FMT_WITH_OPTIONS: phf::Map<&'static str, &'static str> = phf::phf_map! {
4444
"ArrowFunctionExpression" => "FormatJsArrowFunctionExpressionOptions",

0 commit comments

Comments
 (0)