Skip to content

Commit 6cfce80

Browse files
committed
feat(formatter): implement formatting for TSTypeAliasDeclaration (#14040)
1 parent 3097b60 commit 6cfce80

File tree

10 files changed

+194
-235
lines changed

10 files changed

+194
-235
lines changed

crates/oxc_formatter/src/utils/assignment_like.rs

Lines changed: 84 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::{
2020
write,
2121
write::{
2222
BinaryLikeExpression, FormatJsArrowFunctionExpression,
23-
FormatJsArrowFunctionExpressionOptions,
23+
FormatJsArrowFunctionExpressionOptions, FormatWrite,
2424
},
2525
};
2626

@@ -32,7 +32,7 @@ pub enum AssignmentLike<'a, 'b> {
3232
AssignmentExpression(&'b AstNode<'a, AssignmentExpression<'a>>),
3333
ObjectProperty(&'b AstNode<'a, ObjectProperty<'a>>),
3434
PropertyDefinition(&'b AstNode<'a, PropertyDefinition<'a>>),
35-
// TODO: Add TSTypeAliasDeclaration when needed
35+
TSTypeAliasDeclaration(&'b AstNode<'a, TSTypeAliasDeclaration<'a>>),
3636
}
3737

3838
/// Determines how a assignment like be formatted
@@ -224,6 +224,18 @@ impl<'a> AssignmentLike<'a, '_> {
224224

225225
Ok(false) // Class properties don't use "short" key logic
226226
}
227+
AssignmentLike::TSTypeAliasDeclaration(declaration) => {
228+
write!(
229+
f,
230+
[
231+
declaration.declare.then_some("declare "),
232+
"type ",
233+
declaration.id(),
234+
declaration.type_parameters()
235+
]
236+
)?;
237+
Ok(false)
238+
}
227239
}
228240
}
229241

@@ -244,6 +256,9 @@ impl<'a> AssignmentLike<'a, '_> {
244256
{
245257
write!(f, [space(), "="])
246258
}
259+
Self::TSTypeAliasDeclaration(_) => {
260+
write!(f, [space(), "="])
261+
}
247262
_ => Ok(()),
248263
}
249264
}
@@ -274,6 +289,14 @@ impl<'a> AssignmentLike<'a, '_> {
274289
[space(), with_assignment_layout(property.value().unwrap(), Some(layout))]
275290
)
276291
}
292+
Self::TSTypeAliasDeclaration(declaration) => {
293+
if let AstNodes::TSUnionType(union) = declaration.type_annotation().as_ast_nodes() {
294+
union.write(f)?;
295+
union.format_trailing_comments(f)
296+
} else {
297+
write!(f, [space(), declaration.type_annotation()])
298+
}
299+
}
277300
}
278301
}
279302

@@ -305,14 +328,14 @@ impl<'a> AssignmentLike<'a, '_> {
305328
return AssignmentLikeLayout::NeverBreakAfterOperator;
306329
}
307330

308-
if self.should_break_left_hand_side() {
309-
return AssignmentLikeLayout::BreakLeftHandSide;
310-
}
311-
312331
if self.should_break_after_operator(right_expression, f) {
313332
return AssignmentLikeLayout::BreakAfterOperator;
314333
}
315334

335+
if self.should_break_left_hand_side() {
336+
return AssignmentLikeLayout::BreakLeftHandSide;
337+
}
338+
316339
if is_left_short {
317340
return AssignmentLikeLayout::NeverBreakAfterOperator;
318341
}
@@ -370,14 +393,15 @@ impl<'a> AssignmentLike<'a, '_> {
370393
AssignmentLike::PropertyDefinition(property_class_member) => {
371394
property_class_member.value()
372395
}
396+
AssignmentLike::TSTypeAliasDeclaration(declaration) => None,
373397
}
374398
}
375399

376400
/// Checks that a [AssignmentLike] consists only of the left part
377401
/// usually, when a [variable declarator](VariableDeclarator) doesn't have initializer
378402
fn has_only_left_hand_side(&self) -> bool {
379403
match self {
380-
Self::AssignmentExpression(_) => false,
404+
Self::AssignmentExpression(_) | Self::TSTypeAliasDeclaration(_) => false,
381405
Self::VariableDeclarator(declarator) => declarator.init.is_none(),
382406
Self::PropertyDefinition(property) => property.value().is_none(),
383407
Self::ObjectProperty(property) => property.shorthand,
@@ -441,17 +465,20 @@ impl<'a> AssignmentLike<'a, '_> {
441465
return true;
442466
}
443467

444-
// TODO: Add is_complex_type_alias when TypeAliasDeclaration is supported
445-
let is_complex_type_alias = false;
468+
if self.is_complex_type_alias() {
469+
return true;
470+
}
446471

447-
if !self
448-
.get_right_expression()
449-
.is_some_and(|expr| matches!(expr.as_ref(), Expression::ArrowFunctionExpression(_)))
450-
{
472+
let Self::VariableDeclarator(declarator) = self else {
451473
return false;
452-
}
474+
};
453475

454-
matches!(self, Self::VariableDeclarator(decl) if decl.id.type_annotation.as_ref().is_some_and(|ann| is_complex_type_annotation(ann)))
476+
let type_annotation = declarator.id.type_annotation.as_ref();
477+
478+
type_annotation.is_some_and(|ann| is_complex_type_annotation(ann))
479+
|| (self.get_right_expression().is_some_and(|expr| {
480+
matches!(expr.as_ref(), Expression::ArrowFunctionExpression(_))
481+
}) && type_annotation.is_some_and(|ann| is_annotation_breakable(ann)))
455482
}
456483

457484
/// Checks if the current assignment is eligible for [AssignmentLikeLayout::BreakAfterOperator]
@@ -466,33 +493,49 @@ impl<'a> AssignmentLike<'a, '_> {
466493
let comments = f.context().comments();
467494
if let Some(right_expression) = right_expression {
468495
should_break_after_operator(right_expression, f)
496+
} else if let AssignmentLike::TSTypeAliasDeclaration(decl) = self {
497+
// For TSTypeAliasDeclaration, check if the type annotation is a union type with comments
498+
match &decl.type_annotation {
499+
TSType::TSConditionalType(conditional_type) => {
500+
let is_generic = |ts_type: &TSType<'a>| -> bool {
501+
match ts_type {
502+
TSType::TSFunctionType(function) => function.type_parameters.is_some(),
503+
TSType::TSTypeReference(reference) => {
504+
reference.type_arguments.is_some()
505+
}
506+
_ => false,
507+
}
508+
};
509+
510+
is_generic(&conditional_type.check_type)
511+
|| is_generic(&conditional_type.extends_type)
512+
}
513+
_ => {
514+
// Check for leading comments on any other type
515+
comments.has_comment_before(decl.type_annotation.span().start)
516+
}
517+
}
469518
} else {
470-
// RightAssignmentLike::AnyTsType(AnyTsType::TsUnionType(ty)) => {
471-
// // Recursively checks if the union type is nested and identifies the innermost union type.
472-
// // If a leading comment is found while navigating to the inner union type,
473-
// // it is considered as having leading comments.
474-
// let mut union_type = ty.clone();
475-
// let mut has_leading_comments = comments.has_leading_comments(union_type.syntax());
476-
// while is_nested_union_type(&union_type)? && !has_leading_comments {
477-
// if let Some(Ok(inner_union_type)) = union_type.types().last() {
478-
// let inner_union_type = TsUnionType::cast(inner_union_type.into_syntax());
479-
// if let Some(inner_union_type) = inner_union_type {
480-
// has_leading_comments =
481-
// comments.has_leading_comments(inner_union_type.syntax());
482-
// union_type = inner_union_type;
483-
// } else {
484-
// break;
485-
// }
486-
// } else {
487-
// break;
488-
// }
489-
// }
490-
// has_leading_comments
491-
// }
492519
false
493520
}
494521
}
495522

523+
fn is_complex_type_alias(&self) -> bool {
524+
let AssignmentLike::TSTypeAliasDeclaration(type_alias) = self else {
525+
return false;
526+
};
527+
528+
let Some(type_parameters) = &type_alias.type_parameters else {
529+
return false;
530+
};
531+
532+
type_parameters.params.len() > 1
533+
&& type_parameters
534+
.params
535+
.iter()
536+
.any(|param| param.constraint.is_some() || param.default.is_some())
537+
}
538+
496539
fn is_complex_destructuring(&self) -> bool {
497540
match self {
498541
AssignmentLike::VariableDeclarator(variable_decorator) => {
@@ -522,7 +565,9 @@ impl<'a> AssignmentLike<'a, '_> {
522565
AssignmentTargetProperty::AssignmentTargetPropertyProperty(_) => true,
523566
})
524567
}
525-
AssignmentLike::ObjectProperty(_) | AssignmentLike::PropertyDefinition(_) => false,
568+
AssignmentLike::ObjectProperty(_)
569+
| AssignmentLike::PropertyDefinition(_)
570+
| AssignmentLike::TSTypeAliasDeclaration(_) => false,
526571
}
527572
}
528573
}
@@ -883,10 +928,7 @@ fn is_complex_type_arguments(type_arguments: &TSTypeParameterInstantiation) -> b
883928
let is_first_argument_complex = ts_type_argument_list.first().is_some_and(|first_argument| {
884929
matches!(
885930
first_argument,
886-
TSType::TSUnionType(_)
887-
| TSType::TSIntersectionType(_)
888-
| TSType::TSTupleType(_)
889-
| TSType::TSTypeLiteral(_)
931+
TSType::TSUnionType(_) | TSType::TSIntersectionType(_) | TSType::TSTypeLiteral(_)
890932
)
891933
});
892934

@@ -900,20 +942,6 @@ fn is_complex_type_arguments(type_arguments: &TSTypeParameterInstantiation) -> b
900942
false
901943
}
902944

903-
/// If a union type has only one type and it's a union type, then it's a nested union type
904-
/// ```js
905-
/// type A = | (A | B)
906-
/// ^^^^^^^^^^
907-
/// ```
908-
/// The final format will only keep the inner union type
909-
fn is_nested_union_type(union_type: &TSUnionType) -> bool {
910-
if union_type.types.len() == 1 {
911-
let ty = &union_type.types[0];
912-
return matches!(ty, TSType::TSUnionType(_));
913-
}
914-
false
915-
}
916-
917945
/// Checks if the annotation is breakable
918946
fn is_annotation_breakable(annotation: &TSTypeAnnotation) -> bool {
919947
matches!(

crates/oxc_formatter/src/utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod object;
88
pub mod string_utils;
99
pub mod suppressed;
1010
pub mod typecast;
11+
pub mod typescript;
1112

1213
use oxc_allocator::Address;
1314
use oxc_ast::{AstKind, ast::CallExpression};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use oxc_ast::ast::{TSType, TSUnionType};
2+
use oxc_span::GetSpan;
3+
4+
use crate::{formatter::Formatter, generated::ast_nodes::AstNode};
5+
6+
/// Check if a TSType is a simple type (primitives, keywords, simple references)
7+
pub fn is_simple_type(ty: &TSType) -> bool {
8+
match ty {
9+
TSType::TSAnyKeyword(_)
10+
| TSType::TSNullKeyword(_)
11+
| TSType::TSThisType(_)
12+
| TSType::TSVoidKeyword(_)
13+
| TSType::TSNumberKeyword(_)
14+
| TSType::TSBooleanKeyword(_)
15+
| TSType::TSBigIntKeyword(_)
16+
| TSType::TSStringKeyword(_)
17+
| TSType::TSSymbolKeyword(_)
18+
| TSType::TSNeverKeyword(_)
19+
| TSType::TSObjectKeyword(_)
20+
| TSType::TSUndefinedKeyword(_)
21+
| TSType::TSTemplateLiteralType(_)
22+
| TSType::TSLiteralType(_)
23+
| TSType::TSUnknownKeyword(_) => true,
24+
TSType::TSTypeReference(reference) => {
25+
// Simple reference without type arguments
26+
reference.type_arguments.is_none()
27+
}
28+
_ => false,
29+
}
30+
}
31+
32+
/// Check if a TSType is object-like (object literal, mapped type, etc.)
33+
pub fn is_object_like_type(ty: &TSType) -> bool {
34+
matches!(ty, TSType::TSTypeLiteral(_) | TSType::TSMappedType(_))
35+
}
36+
37+
pub fn should_hug_type(node: &TSUnionType<'_>, f: &Formatter<'_, '_>) -> bool {
38+
let types = &node.types;
39+
40+
if types.len() == 1 {
41+
return true;
42+
}
43+
44+
let has_object_type =
45+
types.iter().any(|t| matches!(t, TSType::TSTypeLiteral(_) | TSType::TSTypeReference(_)));
46+
47+
if !has_object_type {
48+
return false;
49+
}
50+
51+
let void_count = types
52+
.iter()
53+
.filter(|t| matches!(t, TSType::TSVoidKeyword(_) | TSType::TSNullKeyword(_)))
54+
.count();
55+
56+
if types.len() - 1 != void_count {
57+
return false;
58+
}
59+
60+
// `{ a: string } /* comment */ | null | /* comment */ */ undefined`
61+
// ^^^^^^^^^^^^ ^^^^^^^^^^^^
62+
// Check whether there are comments between the types, if so, we should not hug
63+
let mut start = node.span.start;
64+
for t in types {
65+
if f.comments().has_comment_in_range(start, t.span().start) {
66+
return false;
67+
}
68+
start = t.span().end;
69+
}
70+
71+
true
72+
}

crates/oxc_formatter/src/write/call_arguments.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -299,11 +299,14 @@ fn should_group_last_argument(
299299
match last.and_then(|arg| arg.as_expression()) {
300300
Some(last) => {
301301
let penultimate = iter.next_back();
302-
if let Some(penultimate) = &penultimate {
303-
// TODO: check if both last and penultimate are same kind of expression.
304-
// if penultimate.syntax().kind() == last.syntax().kind() {
305-
// return Ok(false);
306-
// }
302+
if let Some(penultimate) = &penultimate
303+
&& matches!(
304+
(penultimate, last),
305+
(Argument::ObjectExpression(_), Expression::ObjectExpression(_))
306+
| (Argument::ArrayExpression(_), Expression::ArrayExpression(_))
307+
)
308+
{
309+
return false;
307310
}
308311

309312
let previous_span = penultimate.map_or(call_like_span.start, |a| a.span().end);

0 commit comments

Comments
 (0)