11use oxc_ast:: {
22 AstKind ,
3- ast:: { Expression , TSType , TSTypeAnnotation , TSTypeName } ,
3+ ast:: {
4+ BindingPattern , Expression , NewExpression , TSType , TSTypeAnnotation , TSTypeName ,
5+ TSTypeParameterInstantiation , TSTypeReference ,
6+ } ,
47} ;
58use oxc_diagnostics:: OxcDiagnostic ;
69use oxc_macros:: declare_oxc_lint;
7- use oxc_span:: Span ;
10+ use oxc_span:: { GetSpan , Span } ;
811use schemars:: JsonSchema ;
912use serde:: { Deserialize , Serialize } ;
1013
11- use crate :: { AstNode , context:: LintContext , rule:: Rule } ;
14+ use crate :: { AstNode , context:: LintContext , fixer :: RuleFixer , rule:: Rule } ;
1215
1316fn consistent_generic_constructors_diagnostic_prefer_annotation ( span : Span ) -> OxcDiagnostic {
1417 OxcDiagnostic :: warn (
@@ -88,7 +91,7 @@ declare_oxc_lint!(
8891 ConsistentGenericConstructors ,
8992 typescript,
9093 style,
91- pending ,
94+ fix ,
9295 config = ConsistentGenericConstructorsConfig
9396) ;
9497
@@ -98,17 +101,17 @@ impl Rule for ConsistentGenericConstructors {
98101 AstKind :: VariableDeclarator ( variable_declarator) => {
99102 let type_ann = variable_declarator. type_annotation . as_ref ( ) ;
100103 let init = variable_declarator. init . as_ref ( ) ;
101- self . check ( type_ann, init, ctx) ;
104+ self . check ( node , type_ann, init, ctx) ;
102105 }
103106 AstKind :: FormalParameter ( formal_parameter) => {
104107 let type_ann = formal_parameter. type_annotation . as_ref ( ) ;
105108 let init = formal_parameter. initializer . as_deref ( ) ;
106- self . check ( type_ann, init, ctx) ;
109+ self . check ( node , type_ann, init, ctx) ;
107110 }
108111 AstKind :: PropertyDefinition ( property_definition) => {
109112 let type_ann = property_definition. type_annotation . as_ref ( ) ;
110113 let init = property_definition. value . as_ref ( ) ;
111- self . check ( type_ann, init, ctx) ;
114+ self . check ( node , type_ann, init, ctx) ;
112115 }
113116 _ => { }
114117 }
@@ -130,11 +133,12 @@ impl Rule for ConsistentGenericConstructors {
130133}
131134
132135impl ConsistentGenericConstructors {
133- fn check (
136+ fn check < ' a > (
134137 & self ,
135- type_annotation : Option < & oxc_allocator:: Box < TSTypeAnnotation > > ,
136- init : Option < & Expression > ,
137- ctx : & LintContext ,
138+ node : & AstNode < ' a > ,
139+ type_annotation : Option < & oxc_allocator:: Box < ' a , TSTypeAnnotation < ' a > > > ,
140+ init : Option < & Expression < ' a > > ,
141+ ctx : & LintContext < ' a > ,
138142 ) {
139143 let Some ( init) = init else { return } ;
140144 let Expression :: NewExpression ( new_expression) = init. get_inner_expression ( ) else {
@@ -161,28 +165,223 @@ impl ConsistentGenericConstructors {
161165 if type_annotation. is_none ( )
162166 && let Some ( type_arguments) = & new_expression. type_arguments
163167 {
164- ctx. diagnostic ( consistent_generic_constructors_diagnostic_prefer_annotation (
165- type_arguments. span ,
166- ) ) ;
168+ ctx. diagnostic_with_fix (
169+ consistent_generic_constructors_diagnostic_prefer_annotation (
170+ type_arguments. span ,
171+ ) ,
172+ |fixer| {
173+ Self :: fix_prefer_type_annotation (
174+ fixer,
175+ node,
176+ new_expression,
177+ type_arguments,
178+ ctx,
179+ )
180+ } ,
181+ ) ;
167182 }
168183 return ;
169184 }
170185
171- if let Some ( type_arguments) = & type_annotation
172- && has_type_parameters ( & type_arguments. type_annotation )
186+ if let Some ( type_ann) = & type_annotation
187+ && let TSType :: TSTypeReference ( type_ref) = & type_ann. type_annotation
188+ && let Some ( type_params) = & type_ref. type_arguments
173189 && new_expression. type_arguments . is_none ( )
174190 {
175- ctx. diagnostic ( consistent_generic_constructors_diagnostic_prefer_constructor (
176- type_arguments. span ,
177- ) ) ;
191+ ctx. diagnostic_with_fix (
192+ consistent_generic_constructors_diagnostic_prefer_constructor ( type_ann. span ) ,
193+ |fixer| {
194+ Self :: fix_prefer_constructor (
195+ fixer,
196+ type_ann,
197+ type_ref,
198+ type_params,
199+ new_expression,
200+ ctx,
201+ )
202+ } ,
203+ ) ;
178204 }
179205 }
180- }
181206
182- fn has_type_parameters ( ts_type : & TSType ) -> bool {
183- match ts_type {
184- TSType :: TSTypeReference ( type_ref) => type_ref. type_arguments . is_some ( ) ,
185- _ => false ,
207+ /// Fix for "prefer constructor" mode:
208+ /// Move type arguments from annotation to constructor
209+ /// e.g., `const a: Foo<string> = new Foo()` -> `const a = new Foo<string>()`
210+ fn fix_prefer_constructor < ' a > (
211+ fixer : RuleFixer < ' _ , ' a > ,
212+ type_ann : & TSTypeAnnotation < ' a > ,
213+ type_ref : & TSTypeReference < ' a > ,
214+ type_params : & TSTypeParameterInstantiation < ' a > ,
215+ new_expression : & NewExpression < ' a > ,
216+ ctx : & LintContext < ' a > ,
217+ ) -> crate :: fixer:: RuleFix {
218+ let fixer = fixer. for_multifix ( ) ;
219+ let source_text = ctx. source_text ( ) ;
220+
221+ // Get the type arguments text
222+ let type_params_text =
223+ & source_text[ type_params. span . start as usize ..type_params. span . end as usize ] ;
224+
225+ // Extract comments from two regions in the type annotation:
226+ // 1. Between the colon and type name: `: /* comment */ Foo`
227+ // 2. Between type name and type arguments: `Foo/* another */<string>`
228+ let colon_pos =
229+ ctx. find_prev_token_from ( type_ann. span . start , ":" ) . unwrap_or ( type_ann. span . start ) ;
230+ let type_name_start = type_ref. type_name . span ( ) . start ;
231+ let type_name_end = type_ref. type_name . span ( ) . end ;
232+
233+ // Comments before type name (between colon and type name)
234+ let comments_before: String = ctx
235+ . comments_range ( ( colon_pos + 1 ) ..type_name_start)
236+ . map ( |c| c. span . source_text ( source_text) )
237+ . collect ( ) ;
238+
239+ // Comments between type name and type arguments
240+ let comments_between: String = ctx
241+ . comments_range ( type_name_end..type_params. span . start )
242+ . map ( |c| c. span . source_text ( source_text) )
243+ . collect ( ) ;
244+
245+ // Build the new type arguments string to insert after constructor callee
246+ let new_type_args = format ! ( "{comments_before}{comments_between}{type_params_text}" ) ;
247+
248+ // Determine where to delete the type annotation (including the colon)
249+ let delete_start =
250+ ctx. find_prev_token_from ( type_ann. span . start , ":" ) . unwrap_or ( type_ann. span . start ) ;
251+ let delete_span = Span :: new ( delete_start, type_ann. span . end ) ;
252+
253+ // Find where to insert type arguments in the new expression
254+ let callee_end = new_expression. callee . span ( ) . end ;
255+
256+ // Check if `new Foo;` (no parentheses) - need to handle this case
257+ // If the expression span ends at the callee span end (or type args if present), no parens
258+ let expr_end = new_expression. span . end ;
259+ let callee_or_type_end =
260+ new_expression. type_arguments . as_ref ( ) . map_or ( callee_end, |ta| ta. span . end ) ;
261+ // Look for opening paren after the callee/type args
262+ let after_callee = & source_text[ callee_or_type_end as usize ..expr_end as usize ] ;
263+ let needs_parens = !after_callee. contains ( '(' ) ;
264+
265+ // Build the fix
266+ let mut fix = fixer. new_fix_with_capacity ( if needs_parens { 3 } else { 2 } ) ;
267+
268+ // Delete the type annotation (including leading colon and whitespace)
269+ fix. push ( fixer. delete_range ( delete_span) ) ;
270+
271+ // Insert type arguments after callee
272+ fix. push ( fixer. insert_text_after_range ( Span :: new ( callee_end, callee_end) , new_type_args) ) ;
273+
274+ // If `new Foo;`, add empty parentheses at the end
275+ if needs_parens {
276+ let expr_end = new_expression. span . end ;
277+ fix. push ( fixer. insert_text_after_range ( Span :: new ( expr_end, expr_end) , "()" ) ) ;
278+ }
279+
280+ fix. with_message ( "Move the generic type to the constructor" )
281+ }
282+
283+ /// Fix for "prefer type annotation" mode:
284+ /// Move type arguments from constructor to annotation
285+ /// e.g., `const a = new Foo<string>()` -> `const a: Foo<string> = new Foo()`
286+ fn fix_prefer_type_annotation < ' a > (
287+ fixer : RuleFixer < ' _ , ' a > ,
288+ node : & AstNode < ' a > ,
289+ new_expression : & NewExpression < ' a > ,
290+ type_args : & TSTypeParameterInstantiation < ' a > ,
291+ ctx : & LintContext < ' a > ,
292+ ) -> crate :: fixer:: RuleFix {
293+ let fixer = fixer. for_multifix ( ) ;
294+ let source_text = ctx. source_text ( ) ;
295+
296+ // Get the callee name (constructor name)
297+ let Expression :: Identifier ( callee_ident) = & new_expression. callee else {
298+ return fixer. noop ( ) ;
299+ } ;
300+ let callee_name = callee_ident. name . as_str ( ) ;
301+
302+ // Get type arguments text (including any internal comments)
303+ let type_args_text =
304+ & source_text[ type_args. span . start as usize ..type_args. span . end as usize ] ;
305+
306+ // Build the type annotation to insert (no comments between name and type args)
307+ let type_annotation = format ! ( ": {callee_name}{type_args_text}" ) ;
308+
309+ // Find the position to insert the type annotation (after the binding pattern/identifier)
310+ let Some ( insert_pos) = Self :: find_type_annotation_insert_position ( node, ctx) else {
311+ return fixer. noop ( ) ;
312+ } ;
313+
314+ // For the constructor, we need to delete just the type arguments (the `<...>` part)
315+ // but keep any comments that were between callee and type args
316+ // For `new Foo/* comment */ <string> /* another */()`, we delete ` <string>` (with leading space)
317+ // and keep `/* comment */` and `/* another */`
318+ let callee_end = new_expression. callee . span ( ) . end ;
319+
320+ // Check if there are only whitespace/comments between callee and type args
321+ // If there's whitespace before `<`, we delete from callee end to type args end
322+ // and replace with just the comments (if any)
323+ let comments_between: String = ctx
324+ . comments_range ( callee_end..type_args. span . start )
325+ . map ( |c| c. span . source_text ( source_text) )
326+ . collect ( ) ;
327+
328+ // The delete span is from callee end to type args end
329+ let delete_span = Span :: new ( callee_end, type_args. span . end ) ;
330+
331+ // Replacement: keep comments but remove type args
332+ let replacement =
333+ if comments_between. is_empty ( ) { String :: new ( ) } else { comments_between } ;
334+
335+ // Build the fix
336+ let mut fix = fixer. new_fix_with_capacity ( 2 ) ;
337+
338+ // Insert type annotation after binding
339+ fix. push ( fixer. insert_text_after_range ( Span :: new ( insert_pos, insert_pos) , type_annotation) ) ;
340+
341+ // Replace the type arguments area (callee_end to type_args end) with just comments
342+ fix. push ( fixer. replace ( delete_span, replacement) ) ;
343+
344+ fix. with_message ( "Move the generic type to the type annotation" )
345+ }
346+
347+ /// Find the position to insert a type annotation for the current node
348+ fn find_type_annotation_insert_position (
349+ node : & AstNode < ' _ > ,
350+ ctx : & LintContext < ' _ > ,
351+ ) -> Option < u32 > {
352+ match node. kind ( ) {
353+ AstKind :: VariableDeclarator ( var_decl) => {
354+ // Insert after the binding identifier/pattern
355+ Some ( var_decl. id . span ( ) . end )
356+ }
357+ AstKind :: FormalParameter ( param) => {
358+ // Insert after the binding pattern
359+ match & param. pattern {
360+ BindingPattern :: BindingIdentifier ( ident) => Some ( ident. span . end ) ,
361+ BindingPattern :: ObjectPattern ( obj) => Some ( obj. span . end ) ,
362+ BindingPattern :: ArrayPattern ( arr) => Some ( arr. span . end ) ,
363+ BindingPattern :: AssignmentPattern ( assign) => {
364+ // For assignment pattern like `a = new Foo<string>()`,
365+ // we need to insert after the left side
366+ Some ( assign. left . span ( ) . end )
367+ }
368+ }
369+ }
370+ AstKind :: PropertyDefinition ( prop_def) => {
371+ // For computed properties like `[a]` or `[a + b]`, we need to insert
372+ // after the closing bracket `]`, not after the key expression
373+ if prop_def. computed {
374+ // Find the closing bracket after the key
375+ let key_end = prop_def. key . span ( ) . end ;
376+ // find_next_token_from returns offset from key_end, add 1 for position after ']'
377+ ctx. find_next_token_from ( key_end, "]" ) . map ( |offset| key_end + offset + 1 )
378+ } else {
379+ // Insert after the property key
380+ Some ( prop_def. key . span ( ) . end )
381+ }
382+ }
383+ _ => None ,
384+ }
186385 }
187386}
188387
@@ -445,7 +644,7 @@ fn test() {
445644 ) ,
446645 ] ;
447646
448- let _fix = vec ! [
647+ let fix = vec ! [
449648 ( "const a: Foo<string> = new Foo();" , "const a = new Foo<string>();" , None ) ,
450649 ( "const a: Map<string, number> = new Map();" , "const a = new Map<string, number>();" , None ) ,
451650 (
@@ -570,7 +769,7 @@ fn test() {
570769 ) ,
571770 (
572771 "const a = new Map <string, number> ();" ,
573- "const a: Map<string, number> = new Map ();" ,
772+ "const a: Map<string, number> = new Map ();" ,
574773 Some ( serde_json:: json!( [ "type-annotation" ] ) ) ,
575774 ) ,
576775 (
@@ -589,7 +788,7 @@ fn test() {
589788 ) ,
590789 (
591790 "const a = new Foo/* comment */ <string> /* another */();" ,
592- "const a: Foo<string> = new Foo/* comment */ /* another */();" ,
791+ "const a: Foo<string> = new Foo/* comment */ /* another */();" ,
593792 Some ( serde_json:: json!( [ "type-annotation" ] ) ) ,
594793 ) ,
595794 (
@@ -692,5 +891,6 @@ fn test() {
692891 pass,
693892 fail,
694893 )
894+ . expect_fix ( fix)
695895 . test_and_snapshot ( ) ;
696896}
0 commit comments