Skip to content

Commit 3b7f260

Browse files
committed
feat(linter/consistent-generic-constructor): implement fixer (#18616)
1 parent 6262fcf commit 3b7f260

1 file changed

Lines changed: 227 additions & 27 deletions

File tree

crates/oxc_linter/src/rules/typescript/consistent_generic_constructors.rs

Lines changed: 227 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
use oxc_ast::{
22
AstKind,
3-
ast::{Expression, TSType, TSTypeAnnotation, TSTypeName},
3+
ast::{
4+
BindingPattern, Expression, NewExpression, TSType, TSTypeAnnotation, TSTypeName,
5+
TSTypeParameterInstantiation, TSTypeReference,
6+
},
47
};
58
use oxc_diagnostics::OxcDiagnostic;
69
use oxc_macros::declare_oxc_lint;
7-
use oxc_span::Span;
10+
use oxc_span::{GetSpan, Span};
811
use schemars::JsonSchema;
912
use serde::{Deserialize, Serialize};
1013

11-
use crate::{AstNode, context::LintContext, rule::Rule};
14+
use crate::{AstNode, context::LintContext, fixer::RuleFixer, rule::Rule};
1215

1316
fn 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

132135
impl 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

Comments
 (0)