1
+ use std:: fmt:: Pointer ;
2
+
1
3
use oxc_allocator:: { Address , Vec } ;
2
4
use oxc_ast:: { AstKind , ast:: * } ;
3
5
@@ -9,9 +11,49 @@ use crate::{
9
11
} ,
10
12
generated:: ast_nodes:: { AstNode , AstNodes } ,
11
13
options:: { FormatTrailingCommas , TrailingSeparator } ,
14
+ utils:: call_expression:: is_test_call_expression,
12
15
write,
13
16
} ;
14
17
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
+
15
57
impl < ' a > Format < ' a > for AstNode < ' a , Vec < ' a , TSTypeParameter < ' a > > > {
16
58
fn fmt ( & self , f : & mut Formatter < ' _ , ' a > ) -> FormatResult < ( ) > {
17
59
// 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>>> {
37
79
}
38
80
}
39
81
40
- pub struct FormatTsTypeParametersOptions {
82
+ #[ derive( Default ) ]
83
+ pub struct FormatTSTypeParametersOptions {
41
84
pub group_id : Option < GroupId > ,
42
85
pub is_type_or_interface_decl : bool ,
43
86
}
44
87
45
- pub struct FormatTsTypeParameters < ' a , ' b > {
88
+ pub struct FormatTSTypeParameters < ' a , ' b > {
46
89
decl : & ' b AstNode < ' a , TSTypeParameterDeclaration < ' a > > ,
47
- options : FormatTsTypeParametersOptions ,
90
+ options : FormatTSTypeParametersOptions ,
48
91
}
49
92
50
- impl < ' a , ' b > FormatTsTypeParameters < ' a , ' b > {
93
+ impl < ' a , ' b > FormatTSTypeParameters < ' a , ' b > {
51
94
pub fn new (
52
95
decl : & ' b AstNode < ' a , TSTypeParameterDeclaration < ' a > > ,
53
- options : FormatTsTypeParametersOptions ,
96
+ options : FormatTSTypeParametersOptions ,
54
97
) -> Self {
55
98
Self { decl, options }
56
99
}
57
100
}
58
101
59
- impl < ' a > Format < ' a > for FormatTsTypeParameters < ' a , ' _ > {
102
+ impl < ' a > Format < ' a > for FormatTSTypeParameters < ' a , ' _ > {
60
103
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 {
62
106
write ! ( f, "<>" )
63
107
} else {
64
108
write ! (
65
109
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
+ } ) , ">" ) )
67
118
. with_group_id( self . options. group_id) ]
68
119
)
69
120
}
70
121
}
71
122
}
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
+ }
0 commit comments