1+ use std:: fmt:: Pointer ;
2+
13use oxc_allocator:: { Address , Vec } ;
24use 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+
1557impl < ' 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+ }
0 commit comments