Skip to content

Commit ee15f7d

Browse files
committed
fix(linter): false negative in typescript/prefer-function-type (#11674)
fixes #11668
1 parent f1f3c30 commit ee15f7d

File tree

2 files changed

+79
-72
lines changed

2 files changed

+79
-72
lines changed

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

Lines changed: 72 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,16 @@ fn has_one_super_type(decl: &TSInterfaceDeclaration) -> bool {
104104
}
105105

106106
fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>) {
107-
let TSSignature::TSCallSignatureDeclaration(decl) = member else {
108-
return;
107+
let (span, return_type) = match member {
108+
TSSignature::TSConstructSignatureDeclaration(decl) => (decl.span, &decl.return_type),
109+
TSSignature::TSCallSignatureDeclaration(decl) => (decl.span, &decl.return_type),
110+
_ => return,
109111
};
110-
let Some(type_annotation) = &decl.return_type else {
112+
113+
let Some(type_annotation) = &return_type else {
111114
return;
112115
};
113-
let Span { start, end, .. } = decl.span;
116+
let Span { start, end, .. } = span;
114117
let colon_pos = type_annotation.span.start - start;
115118
let source_code = &ctx.source_text();
116119
let text: &str = &source_code[start as usize..end as usize];
@@ -128,7 +131,7 @@ fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>)
128131
AstKind::TSInterfaceDeclaration(interface_decl) => {
129132
if let Some(type_parameters) = &interface_decl.type_parameters {
130133
ctx.diagnostic_with_fix(
131-
prefer_function_type_diagnostic(&suggestion, decl.span),
134+
prefer_function_type_diagnostic(&suggestion, span),
132135
|fixer| {
133136
let mut span = interface_decl.id.span;
134137
span.end = type_parameters.span.end;
@@ -142,76 +145,71 @@ fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>)
142145
},
143146
);
144147
} else {
145-
ctx.diagnostic_with_fix(
146-
prefer_function_type_diagnostic(&suggestion, decl.span),
147-
|_| {
148-
let mut is_parent_exported = false;
149-
let mut node_start = interface_decl.span.start;
150-
let mut node_end = interface_decl.span.end;
151-
if let Some(parent_node) = ctx.nodes().parent_node(node.id()) {
152-
if let AstKind::ExportNamedDeclaration(export_name_decl) =
153-
parent_node.kind()
154-
{
155-
is_parent_exported = true;
156-
node_start = export_name_decl.span.start;
157-
node_end = export_name_decl.span.end;
158-
}
148+
ctx.diagnostic_with_fix(prefer_function_type_diagnostic(&suggestion, span), |_| {
149+
let mut is_parent_exported = false;
150+
let mut node_start = interface_decl.span.start;
151+
let mut node_end = interface_decl.span.end;
152+
if let Some(parent_node) = ctx.nodes().parent_node(node.id()) {
153+
if let AstKind::ExportNamedDeclaration(export_name_decl) =
154+
parent_node.kind()
155+
{
156+
is_parent_exported = true;
157+
node_start = export_name_decl.span.start;
158+
node_end = export_name_decl.span.end;
159159
}
160+
}
160161

161-
let has_comments = ctx.has_comments_between(interface_decl.span);
162-
163-
if has_comments {
164-
let comments = ctx
165-
.comments_range(node_start..node_end)
166-
.map(|comment| (*comment, comment.content_span()));
167-
168-
let comments_text = {
169-
let mut comments_vec: Vec<String> = vec![];
170-
comments.for_each(|(comment_interface, span)| {
171-
let comment = span.source_text(source_code);
172-
173-
match comment_interface.kind {
174-
CommentKind::Line => {
175-
let single_line_comment: String =
176-
format!("//{comment}\n");
177-
comments_vec.push(single_line_comment);
178-
}
179-
CommentKind::Block => {
180-
let multi_line_comment: String =
181-
format!("/*{comment}*/\n");
182-
comments_vec.push(multi_line_comment);
183-
}
162+
let has_comments = ctx.has_comments_between(interface_decl.span);
163+
164+
if has_comments {
165+
let comments = ctx
166+
.comments_range(node_start..node_end)
167+
.map(|comment| (*comment, comment.content_span()));
168+
169+
let comments_text = {
170+
let mut comments_vec: Vec<String> = vec![];
171+
comments.for_each(|(comment_interface, span)| {
172+
let comment = span.source_text(source_code);
173+
174+
match comment_interface.kind {
175+
CommentKind::Line => {
176+
let single_line_comment: String = format!("//{comment}\n");
177+
comments_vec.push(single_line_comment);
184178
}
185-
});
186-
187-
comments_vec.join("")
188-
};
189-
190-
return Fix::new(
191-
format!(
192-
"{}{}{} = {};",
193-
comments_text,
194-
if is_parent_exported { "export type " } else { "type " },
195-
&interface_decl.id.name,
196-
&suggestion
197-
),
198-
Span::new(node_start, node_end),
199-
)
200-
.with_message(CONVERT_TO_FUNCTION_TYPE);
201-
}
179+
CommentKind::Block => {
180+
let multi_line_comment: String = format!("/*{comment}*/\n");
181+
comments_vec.push(multi_line_comment);
182+
}
183+
}
184+
});
185+
186+
comments_vec.join("")
187+
};
202188

203-
Fix::new(
189+
return Fix::new(
204190
format!(
205-
"{} {} = {};",
206-
if is_parent_exported { "export type" } else { "type" },
191+
"{}{}{} = {};",
192+
comments_text,
193+
if is_parent_exported { "export type " } else { "type " },
207194
&interface_decl.id.name,
208195
&suggestion
209196
),
210197
Span::new(node_start, node_end),
211198
)
212-
.with_message(CONVERT_TO_FUNCTION_TYPE)
213-
},
214-
);
199+
.with_message(CONVERT_TO_FUNCTION_TYPE);
200+
}
201+
202+
Fix::new(
203+
format!(
204+
"{} {} = {};",
205+
if is_parent_exported { "export type" } else { "type" },
206+
&interface_decl.id.name,
207+
&suggestion
208+
),
209+
Span::new(node_start, node_end),
210+
)
211+
.with_message(CONVERT_TO_FUNCTION_TYPE)
212+
});
215213
}
216214
}
217215

@@ -221,7 +219,7 @@ fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>)
221219
union_type.types.iter().for_each(|ts_type| {
222220
if let TSType::TSTypeLiteral(literal) = ts_type {
223221
ctx.diagnostic_with_fix(
224-
prefer_function_type_diagnostic(&suggestion, decl.span),
222+
prefer_function_type_diagnostic(&suggestion, span),
225223
|fixer| {
226224
fixer
227225
.replace(literal.span, format!("({suggestion})"))
@@ -233,7 +231,7 @@ fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>)
233231
}
234232

235233
TSType::TSTypeLiteral(literal) => ctx.diagnostic_with_fix(
236-
prefer_function_type_diagnostic(&suggestion, decl.span),
234+
prefer_function_type_diagnostic(&suggestion, span),
237235
|fixer| {
238236
fixer
239237
.replace(literal.span, suggestion)
@@ -242,7 +240,7 @@ fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>)
242240
),
243241

244242
_ => {
245-
ctx.diagnostic(prefer_function_type_diagnostic(&suggestion, decl.span));
243+
ctx.diagnostic(prefer_function_type_diagnostic(&suggestion, span));
246244
}
247245
}
248246
}
@@ -259,7 +257,7 @@ fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>)
259257
return;
260258
}
261259
ctx.diagnostic_with_fix(
262-
prefer_function_type_diagnostic(&suggestion, decl.span),
260+
prefer_function_type_diagnostic(&suggestion, span),
263261
|fixer| {
264262
fixer
265263
.replace(literal.span, format!("({suggestion})"))
@@ -279,7 +277,7 @@ fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>)
279277
return;
280278
}
281279
ctx.diagnostic_with_fix(
282-
prefer_function_type_diagnostic(&suggestion, decl.span),
280+
prefer_function_type_diagnostic(&suggestion, span),
283281
|fixer| {
284282
fixer
285283
.replace(literal.span, format!("({suggestion})"))
@@ -290,7 +288,7 @@ fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>)
290288
}
291289

292290
TSType::TSTypeLiteral(literal) => ctx.diagnostic_with_fix(
293-
prefer_function_type_diagnostic(&suggestion, decl.span),
291+
prefer_function_type_diagnostic(&suggestion, span),
294292
|fixer| {
295293
fixer
296294
.replace(literal.span, suggestion)
@@ -302,7 +300,7 @@ fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>)
302300
}
303301
}
304302

305-
_ => ctx.diagnostic(prefer_function_type_diagnostic(&suggestion, decl.span)),
303+
_ => ctx.diagnostic(prefer_function_type_diagnostic(&suggestion, span)),
306304
}
307305
}
308306

@@ -502,6 +500,7 @@ fn test() {
502500
",
503501
"type X = {} | { (): void; }",
504502
"type X = {} & { (): void; };",
503+
"type K = { new(): T };",
505504
];
506505

507506
let fix = vec![
@@ -720,6 +719,7 @@ type X = {} & (() => void);
720719
"export type AnyFn = (...args: any[]) => any;",
721720
None,
722721
),
722+
("type K = { new(): T };", "type K = new() => T;", None),
723723
];
724724

725725
Tester::new(PreferFunctionType::NAME, PreferFunctionType::PLUGIN, pass, fail)

crates/oxc_linter/src/snapshots/typescript_prefer_function_type.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,10 @@ source: crates/oxc_linter/src/tester.rs
174174
· ─────────
175175
╰────
176176
help: The function type form `() => void` is generally preferred when possible for being more succinct.
177+
178+
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
179+
╭─[prefer_function_type.tsx:1:12]
180+
1 │ type K = { new(): T };
181+
· ────────
182+
╰────
183+
help: The function type form `new() => T` is generally preferred when possible for being more succinct.

0 commit comments

Comments
 (0)