Skip to content

Commit 70c33a9

Browse files
committed
feat(formatter): implement formatting for TSTypeParameter and TSTypeParameterInstantiation
1 parent 0f036bb commit 70c33a9

File tree

7 files changed

+240
-114
lines changed

7 files changed

+240
-114
lines changed

crates/oxc_formatter/src/write/arrow_function_expression.rs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -855,22 +855,15 @@ fn format_signature<'a, 'b>(
855855
cache_mode: FunctionBodyCacheMode,
856856
) -> impl Format<'a> + 'b {
857857
format_with(move |f| {
858-
let formatted_async_token =
859-
format_with(|f| if arrow.r#async() { write!(f, ["async", space()]) } else { Ok(()) });
860-
861-
let formatted_parameters =
862-
format_with(|f| write!(f, [arrow.type_parameters(), arrow.params()]));
863-
864-
let format_return_type = format_with(|f| write!(f, arrow.return_type()));
865-
866858
let signatures = format_once(|f| {
867859
write!(
868860
f,
869861
[group(&format_args!(
870862
maybe_space(!is_first_in_chain),
871-
formatted_async_token,
872-
group(&formatted_parameters),
873-
group(&format_return_type)
863+
arrow.r#async().then_some("async "),
864+
arrow.type_parameters(),
865+
arrow.params(),
866+
group(&arrow.return_type())
874867
))]
875868
)
876869
});

crates/oxc_formatter/src/write/class.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use crate::{
2727

2828
use super::{
2929
FormatWrite,
30-
type_parameters::{FormatTsTypeParameters, FormatTsTypeParametersOptions},
30+
type_parameters::{FormatTSTypeParameters, FormatTSTypeParametersOptions},
3131
};
3232

3333
impl<'a> FormatWrite<'a> for AstNode<'a, ClassBody<'a>> {
@@ -314,9 +314,9 @@ impl<'a> Format<'a> for FormatClass<'a, '_> {
314314
if let Some(type_parameters) = &type_parameters {
315315
write!(
316316
f,
317-
FormatTsTypeParameters::new(
317+
FormatTSTypeParameters::new(
318318
type_parameters,
319-
FormatTsTypeParametersOptions {
319+
FormatTSTypeParametersOptions {
320320
group_id: type_parameters_id,
321321
is_type_or_interface_decl: false
322322
}

crates/oxc_formatter/src/write/function.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ impl<'a> FormatWrite<'a> for FormatFunction<'a, '_> {
4949
if self.r#async() {
5050
write!(f, ["async", space()])?;
5151
}
52+
5253
write!(
5354
f,
5455
[

crates/oxc_formatter/src/write/mod.rs

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ use self::{
7878
object_pattern_like::ObjectPatternLike,
7979
parameter_list::{ParameterLayout, ParameterList},
8080
semicolon::{ClassPropertySemicolon, OptionalSemicolon},
81-
type_parameters::{FormatTsTypeParameters, FormatTsTypeParametersOptions},
81+
type_parameters::{FormatTSTypeParameters, FormatTSTypeParametersOptions},
8282
utils::{
8383
array::{TrailingSeparatorMode, write_array_node},
8484
statement_body::FormatStatementBody,
@@ -1377,44 +1377,9 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSQualifiedName<'a>> {
13771377
}
13781378
}
13791379

1380-
impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeParameterInstantiation<'a>> {
1381-
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
1382-
write!(f, "<")?;
1383-
for (i, param) in self.params().iter().enumerate() {
1384-
if i != 0 {
1385-
write!(f, [",", space()])?;
1386-
}
1387-
write!(f, param)?;
1388-
}
1389-
write!(f, ">")
1390-
}
1391-
}
1392-
1393-
impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeParameter<'a>> {
1394-
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
1395-
if self.r#const() {
1396-
write!(f, ["const", space()])?;
1397-
}
1398-
if self.r#in() {
1399-
write!(f, ["in", space()])?;
1400-
}
1401-
if self.out() {
1402-
write!(f, ["out", space()])?;
1403-
}
1404-
write!(f, self.name())?;
1405-
if let Some(constraint) = &self.constraint() {
1406-
write!(f, [space(), "extends", space(), constraint])?;
1407-
}
1408-
if let Some(default) = &self.default() {
1409-
write!(f, [space(), "=", space(), default])?;
1410-
}
1411-
Ok(())
1412-
}
1413-
}
1414-
14151380
impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeParameterDeclaration<'a>> {
14161381
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
1417-
write!(f, ["<", self.params(), ">"])
1382+
FormatTSTypeParameters::new(self, FormatTSTypeParametersOptions::default()).fmt(f)
14181383
}
14191384
}
14201385

@@ -1463,9 +1428,9 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSInterfaceDeclaration<'a>> {
14631428
if let Some(type_parameters) = type_parameters {
14641429
write!(
14651430
f,
1466-
FormatTsTypeParameters::new(
1431+
FormatTSTypeParameters::new(
14671432
type_parameters,
1468-
FormatTsTypeParametersOptions {
1433+
FormatTSTypeParametersOptions {
14691434
group_id: type_parameter_group,
14701435
is_type_or_interface_decl: true
14711436
}

crates/oxc_formatter/src/write/type_parameters.rs

Lines changed: 205 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::fmt::Pointer;
2+
13
use oxc_allocator::{Address, Vec};
24
use oxc_ast::{AstKind, ast::*};
35

@@ -9,9 +11,49 @@ use crate::{
911
},
1012
generated::ast_nodes::{AstNode, AstNodes},
1113
options::{FormatTrailingCommas, TrailingSeparator},
14+
utils::call_expression::is_test_call_expression,
1215
write,
1316
};
1417

18+
use super::FormatWrite;
19+
20+
impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeParameter<'a>> {
21+
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
22+
if self.r#const() {
23+
write!(f, ["const", space()])?;
24+
}
25+
if self.r#in() {
26+
write!(f, ["in", space()])?;
27+
}
28+
if self.out() {
29+
write!(f, ["out", space()])?;
30+
}
31+
write!(f, self.name())?;
32+
33+
if let Some(constraint) = &self.constraint() {
34+
let group_id = f.group_id("constraint");
35+
36+
write!(
37+
f,
38+
[
39+
space(),
40+
"extends",
41+
group(&indent(&format_args!(
42+
line_suffix_boundary(),
43+
soft_line_break_or_space()
44+
)))
45+
.with_group_id(Some(group_id)),
46+
indent_if_group_breaks(&constraint, group_id)
47+
]
48+
)?;
49+
}
50+
if let Some(default) = &self.default() {
51+
write!(f, [space(), "=", space(), default])?;
52+
}
53+
Ok(())
54+
}
55+
}
56+
1557
impl<'a> Format<'a> for AstNode<'a, Vec<'a, TSTypeParameter<'a>>> {
1658
fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
1759
// Type parameter lists of arrow function expressions have to include at least one comma
@@ -37,35 +79,190 @@ impl<'a> Format<'a> for AstNode<'a, Vec<'a, TSTypeParameter<'a>>> {
3779
}
3880
}
3981

40-
pub struct FormatTsTypeParametersOptions {
82+
#[derive(Default)]
83+
pub struct FormatTSTypeParametersOptions {
4184
pub group_id: Option<GroupId>,
4285
pub is_type_or_interface_decl: bool,
4386
}
4487

45-
pub struct FormatTsTypeParameters<'a, 'b> {
88+
pub struct FormatTSTypeParameters<'a, 'b> {
4689
decl: &'b AstNode<'a, TSTypeParameterDeclaration<'a>>,
47-
options: FormatTsTypeParametersOptions,
90+
options: FormatTSTypeParametersOptions,
4891
}
4992

50-
impl<'a, 'b> FormatTsTypeParameters<'a, 'b> {
93+
impl<'a, 'b> FormatTSTypeParameters<'a, 'b> {
5194
pub fn new(
5295
decl: &'b AstNode<'a, TSTypeParameterDeclaration<'a>>,
53-
options: FormatTsTypeParametersOptions,
96+
options: FormatTSTypeParametersOptions,
5497
) -> Self {
5598
Self { decl, options }
5699
}
57100
}
58101

59-
impl<'a> Format<'a> for FormatTsTypeParameters<'a, '_> {
102+
impl<'a> Format<'a> for FormatTSTypeParameters<'a, '_> {
60103
fn fmt(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
61-
if self.decl.params().is_empty() && self.options.is_type_or_interface_decl {
104+
let params = self.decl.params();
105+
if params.is_empty() && self.options.is_type_or_interface_decl {
62106
write!(f, "<>")
63107
} else {
64108
write!(
65109
f,
66-
[group(&format_args!("<", soft_block_indent(&self.decl.params()), ">"))
110+
[group(&format_args!("<", format_once(|f| {
111+
if matches!( self.decl.parent.parent().parent(), AstNodes::CallExpression(call) if is_test_call_expression(call))
112+
{
113+
f.join_nodes_with_space().entries_with_trailing_separator(params, ",", TrailingSeparator::Omit).finish()
114+
} else {
115+
soft_block_indent(&params).fmt(f)
116+
}
117+
}), ">"))
67118
.with_group_id(self.options.group_id)]
68119
)
69120
}
70121
}
71122
}
123+
124+
impl<'a> FormatWrite<'a> for AstNode<'a, TSTypeParameterInstantiation<'a>> {
125+
fn write(&self, f: &mut Formatter<'_, 'a>) -> FormatResult<()> {
126+
let params = self.params();
127+
128+
if params.is_empty() {
129+
// This shouldn't happen in valid TypeScript code, but handle it gracefully
130+
return write!(
131+
f,
132+
[&group(&format_args!(
133+
"<",
134+
format_dangling_comments(self.span).with_soft_block_indent(),
135+
">"
136+
))]
137+
);
138+
}
139+
140+
// Check if this is in the context of an arrow function variable
141+
let is_arrow_function_vars = is_arrow_function_variable_type_argument(self);
142+
143+
// Check if the first (and only) argument can be hugged
144+
let first_arg_can_be_hugged = if params.len() == 1 {
145+
if let Some(first_type) = params.first() {
146+
matches!(first_type.as_ref(), TSType::TSNullKeyword(_))
147+
|| should_hug_single_type(first_type.as_ref())
148+
} else {
149+
false
150+
}
151+
} else {
152+
false
153+
};
154+
155+
let format_params = format_once(|f| {
156+
f.join_with(&soft_line_break_or_space())
157+
.entries_with_trailing_separator(params, ",", TrailingSeparator::Disallowed)
158+
.finish()
159+
});
160+
161+
let should_inline =
162+
!is_arrow_function_vars && (params.is_empty() || first_arg_can_be_hugged);
163+
164+
if should_inline {
165+
write!(f, ["<", format_params, ">"])
166+
} else {
167+
write!(f, [group(&format_args!("<", soft_block_indent(&format_params), ">"))])
168+
}
169+
}
170+
}
171+
172+
/// Check if a TSType is a simple type (primitives, keywords, simple references)
173+
fn is_simple_type(ty: &TSType) -> bool {
174+
match ty {
175+
TSType::TSAnyKeyword(_)
176+
| TSType::TSNullKeyword(_)
177+
| TSType::TSThisType(_)
178+
| TSType::TSVoidKeyword(_)
179+
| TSType::TSNumberKeyword(_)
180+
| TSType::TSBooleanKeyword(_)
181+
| TSType::TSBigIntKeyword(_)
182+
| TSType::TSStringKeyword(_)
183+
| TSType::TSSymbolKeyword(_)
184+
| TSType::TSNeverKeyword(_)
185+
| TSType::TSObjectKeyword(_)
186+
| TSType::TSUndefinedKeyword(_)
187+
| TSType::TSTemplateLiteralType(_)
188+
| TSType::TSLiteralType(_)
189+
| TSType::TSUnknownKeyword(_) => true,
190+
TSType::TSTypeReference(reference) => {
191+
// Simple reference without type arguments
192+
reference.type_arguments.is_none()
193+
}
194+
_ => false,
195+
}
196+
}
197+
198+
/// Check if a TSType is object-like (object literal, mapped type, etc.)
199+
fn is_object_like_type(ty: &TSType) -> bool {
200+
matches!(ty, TSType::TSTypeLiteral(_) | TSType::TSMappedType(_))
201+
}
202+
203+
/// Check if a single type should be "hugged" (kept inline)
204+
fn should_hug_single_type(ty: &TSType) -> bool {
205+
// Simple types and object-like types can be hugged
206+
if is_simple_type(ty) || is_object_like_type(ty) {
207+
return true;
208+
}
209+
210+
// Check for union types with mostly void types and one object type
211+
// (e.g., `SomeType<ObjectType | null | undefined>`)
212+
if let TSType::TSUnionType(union_type) = ty {
213+
let types = &union_type.types;
214+
215+
// Must have at least 2 types
216+
if types.len() < 2 {
217+
return types.len() == 1 && should_hug_single_type(&types[0]);
218+
}
219+
220+
let has_object_type = types
221+
.iter()
222+
.any(|t| matches!(t, TSType::TSTypeLiteral(_) | TSType::TSTypeReference(_)));
223+
224+
let void_count = types
225+
.iter()
226+
.filter(|t| {
227+
matches!(
228+
t,
229+
TSType::TSVoidKeyword(_)
230+
| TSType::TSNullKeyword(_)
231+
| TSType::TSUndefinedKeyword(_)
232+
)
233+
})
234+
.count();
235+
236+
// Union is huggable if it's mostly void types with one object/reference type
237+
(types.len() - 1 == void_count && has_object_type) || types.len() == 1
238+
} else {
239+
false
240+
}
241+
}
242+
243+
/// Check if this type parameter instantiation is in an arrow function variable context
244+
///
245+
/// This detects patterns like:
246+
/// ```typescript
247+
/// const foo: SomeThing<{ [P in "x" | "y"]: number }> = () => {};
248+
/// ```
249+
fn is_arrow_function_variable_type_argument<'a>(
250+
node: &AstNode<'a, TSTypeParameterInstantiation<'a>>,
251+
) -> bool {
252+
let Some(first) = node.params().first() else { unreachable!() };
253+
254+
// Skip check for single object-like types
255+
if node.params().len() == 1 && is_object_like_type(first.as_ref()) {
256+
return false;
257+
}
258+
259+
matches!(
260+
&node.parent,
261+
AstNodes::TSTypeAnnotation(type_annotation)
262+
if matches!(
263+
&type_annotation.parent,
264+
AstNodes::VariableDeclarator(var_decl)
265+
if matches!(&var_decl.init, Some(Expression::ArrowFunctionExpression(_)))
266+
)
267+
)
268+
}

tasks/coverage/snapshots/formatter_typescript.snap

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ commit: 261630d6
22

33
formatter_typescript Summary:
44
AST Parsed : 8816/8816 (100.00%)
5-
Positive Passed: 8788/8816 (99.68%)
5+
Positive Passed: 8789/8816 (99.69%)
66
Mismatch: tasks/coverage/typescript/tests/cases/compiler/amdLikeInputDeclarationEmit.ts
77

88
Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/arrayFromAsync.ts
@@ -15,8 +15,6 @@ Mismatch: tasks/coverage/typescript/tests/cases/compiler/complexNarrowingWithAny
1515

1616
Mismatch: tasks/coverage/typescript/tests/cases/compiler/declarationEmitCastReusesTypeNode4.ts
1717

18-
Mismatch: tasks/coverage/typescript/tests/cases/compiler/declarationEmitShadowingInferNotRenamed.ts
19-
2018
Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/genericTypeAssertions3.ts
2119
Unexpected token
2220
Mismatch: tasks/coverage/typescript/tests/cases/compiler/jsxNamespaceGlobalReexport.tsx

0 commit comments

Comments
 (0)