zhangrunzhao 2024-02-21 20:15:40 +08:00 committed by GitHub
parent 30392c96e8
commit ff6a337453
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 926 additions and 0 deletions

View file

@ -118,6 +118,7 @@ mod typescript {
pub mod no_unsafe_declaration_merging;
pub mod no_var_requires;
pub mod prefer_as_const;
pub mod prefer_function_type;
pub mod triple_slash_reference;
}
@ -414,6 +415,7 @@ oxc_macros::declare_all_lint_rules! {
typescript::no_unsafe_declaration_merging,
typescript::no_var_requires,
typescript::prefer_as_const,
typescript::prefer_function_type,
typescript::triple_slash_reference,
jest::expect_expect,
jest::max_expects,

View file

@ -0,0 +1,751 @@
use oxc_ast::{
ast::{ExportDefaultDeclarationKind, Expression, TSInterfaceDeclaration, TSSignature, TSType},
AstKind, CommentKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use crate::{context::LintContext, fixer::Fix, rule::Rule, AstNode};
#[derive(Debug, Error, Diagnostic)]
#[error("typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.")]
#[diagnostic(
severity(warning),
help("The function type form `{0}` is generally preferred when possible for being more succinct.")
)]
struct PreferFunctionTypeDiagnostic(String, #[label] pub Span);
#[derive(Debug, Default, Clone)]
pub struct PreferFunctionType;
declare_oxc_lint!(
/// ### What it does
/// Enforce using function types instead of interfaces with call signatures.
/// TypeScript allows for two common ways to declare a type for a function:
///
/// - Function type: `() => string`
/// - Object type with a signature: `{ (): string }`
///
/// The function type form is generally preferred when possible for being more succinct.
///
/// This rule suggests using a function type instead of an interface or object type literal with a single call signature.
///
/// ### Example
/// ```javascript
/// // error
/// interface Example {
/// (): string;
/// }
///
/// function foo(example: { (): number }): number {
/// return example();
/// }
///
/// interface ReturnsSelf {
/// (arg: string): this;
/// }
///
/// // success
/// type Example = () => string;
///
/// function foo(example: () => number): number {
/// return bar();
/// }
///
/// // returns the function itself, not the `this` argument.
/// type ReturnsSelf = (arg: string) => ReturnsSelf;
///
/// function foo(bar: { (): string; baz: number }): string {
/// return bar();
/// }
///
/// interface Foo {
/// bar: string;
/// }
/// interface Bar extends Foo {
/// (): void;
/// }
///
/// // multiple call signatures (overloads) is allowed:
/// interface Overloaded {
/// (data: string): number;
/// (id: number): string;
/// }
/// // this is equivalent to Overloaded interface.
/// type Intersection = ((data: string) => number) & ((id: number) => string);
/// ```
PreferFunctionType,
style
);
fn has_one_super_type(decl: &TSInterfaceDeclaration) -> bool {
if decl.extends.is_none() {
return false;
}
let decl_extends_vec = decl.extends.as_deref().unwrap();
if decl_extends_vec.is_empty() {
return false;
}
if decl_extends_vec.len() != 1 {
return true;
}
let expr = &decl_extends_vec[0].expression;
if let Expression::Identifier(identifier) = expr {
return !matches!(identifier.name.as_str(), "Function");
}
true
}
fn check_member(member: &TSSignature, node: &AstNode<'_>, ctx: &LintContext<'_>) {
match member {
TSSignature::TSCallSignatureDeclaration(decl) => {
let start = decl.span.start;
let end: u32 = decl.span.end;
if let Some(type_annotation) = &decl.return_type {
let colon_pos = type_annotation.span.start - start;
let source_code = &ctx.source_text();
let text: &str = &source_code[start as usize..end as usize];
let mut suggestion = format!(
"{} =>{}",
&text[0..colon_pos as usize],
&text[(colon_pos + 1) as usize..text.len()]
);
if suggestion.ends_with(';') {
suggestion.pop();
}
match node.kind() {
AstKind::TSInterfaceDeclaration(interface_decl) => {
if let Some(type_parameters) = &interface_decl.type_parameters {
let node_start = interface_decl.span.start;
let node_end = interface_decl.span.end;
let type_name = &source_code[interface_decl.id.span.start as usize
..type_parameters.span.end as usize];
ctx.diagnostic_with_fix(
PreferFunctionTypeDiagnostic(suggestion.clone(), decl.span),
|| {
Fix::new(
format!("type {type_name} = {suggestion};"),
Span { start: node_start, end: node_end },
)
},
);
} else {
ctx.diagnostic_with_fix(
PreferFunctionTypeDiagnostic(suggestion.clone(), decl.span),
|| {
let mut is_parent_exported = false;
let mut node_start = interface_decl.span.start;
let mut node_end = interface_decl.span.end;
if let Some(parent_node) = ctx.nodes().parent_node(node.id()) {
if let AstKind::ExportNamedDeclaration(export_name_decl) =
parent_node.kind()
{
is_parent_exported = true;
node_start = export_name_decl.span.start;
node_end = export_name_decl.span.end;
}
}
let has_comments = ctx
.semantic()
.trivias()
.has_comments_between(interface_decl.span);
if has_comments {
let comments = ctx
.semantic()
.trivias()
.comments()
.range(node_start..node_end)
.map(|(start, comment)| {
(*comment, Span::new(*start, comment.end()))
});
let comments_text = {
let mut comments_vec: Vec<String> = vec![];
comments.for_each(|(comment_interface, span)| {
let comment = &source_code
[span.start as usize..span.end as usize];
match comment_interface.kind() {
CommentKind::SingleLine => {
let single_line_comment: String =
format!("//{comment}\n");
comments_vec.push(single_line_comment);
}
CommentKind::MultiLine => {
let multi_line_comment: String =
format!("/*{comment}*/\n");
comments_vec.push(multi_line_comment);
}
}
});
comments_vec.join("")
};
return Fix::new(
format!(
"{}{}{} = {};",
comments_text,
if is_parent_exported {
"export type "
} else {
"type "
},
&interface_decl.id.name,
&suggestion
),
Span { start: node_start, end: node_end },
);
}
Fix::new(
format!(
"type {} = {};",
&interface_decl.id.name, &suggestion
),
Span { start: node_start, end: node_end },
)
},
);
}
}
AstKind::TSTypeAnnotation(ts_type_annotation) => {
match &ts_type_annotation.type_annotation {
TSType::TSUnionType(union_type) => {
union_type.types.iter().for_each(|ts_type| {
if let TSType::TSTypeLiteral(literal) = ts_type {
ctx.diagnostic_with_fix(
PreferFunctionTypeDiagnostic(
suggestion.clone(),
decl.span,
),
|| {
Fix::new(
format!("({suggestion})"),
Span {
start: literal.span.start,
end: literal.span.end,
},
)
},
);
}
});
}
TSType::TSTypeLiteral(literal) => ctx.diagnostic_with_fix(
PreferFunctionTypeDiagnostic(suggestion.clone(), decl.span),
|| {
Fix::new(
suggestion.to_string(),
Span { start: literal.span.start, end: literal.span.end },
)
},
),
_ => {
ctx.diagnostic(PreferFunctionTypeDiagnostic(suggestion, decl.span));
}
}
}
AstKind::TSTypeAliasDeclaration(ts_type_alias_decl) => {
match &ts_type_alias_decl.type_annotation {
TSType::TSUnionType(union_type) => {
union_type.types.iter().for_each(|ts_type| {
if let TSType::TSTypeLiteral(literal) = ts_type {
let body = &literal.members;
if body.len() == 1 {
ctx.diagnostic_with_fix(
PreferFunctionTypeDiagnostic(
suggestion.clone(),
decl.span,
),
|| {
Fix::new(
format!("({suggestion})"),
Span {
start: literal.span.start,
end: literal.span.end,
},
)
},
);
}
}
});
}
TSType::TSIntersectionType(intersection_type) => {
intersection_type.types.iter().for_each(|ts_type| {
if let TSType::TSTypeLiteral(literal) = ts_type {
let body = &literal.members;
if body.len() == 1 {
ctx.diagnostic_with_fix(
PreferFunctionTypeDiagnostic(
suggestion.clone(),
decl.span,
),
|| {
Fix::new(
format!("({suggestion})"),
Span {
start: literal.span.start,
end: literal.span.end,
},
)
},
);
}
}
});
}
TSType::TSTypeLiteral(literal) => ctx.diagnostic_with_fix(
PreferFunctionTypeDiagnostic(suggestion.clone(), decl.span),
|| {
Fix::new(
suggestion.to_string(),
Span { start: literal.span.start, end: literal.span.end },
)
},
),
_ => {}
}
}
_ => ctx.diagnostic(PreferFunctionTypeDiagnostic(suggestion, decl.span)),
}
}
}
TSSignature::TSConstructSignatureDeclaration(_)
| TSSignature::TSIndexSignature(_)
| TSSignature::TSPropertySignature(_)
| TSSignature::TSMethodSignature(_) => {}
}
}
impl Rule for PreferFunctionType {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
AstKind::TSInterfaceDeclaration(decl) => {
let body: &oxc_allocator::Vec<'_, TSSignature<'_>> = &decl.body.body;
if !has_one_super_type(decl) && body.len() == 1 {
check_member(&body[0], node, ctx);
}
}
AstKind::ExportDefaultDeclaration(decl) => {
if let ExportDefaultDeclarationKind::TSInterfaceDeclaration(interface_decl) =
&decl.declaration
{
let body = &interface_decl.body.body;
if !has_one_super_type(interface_decl) && body.len() == 1 {
check_member(&body[0], node, ctx);
}
}
}
AstKind::TSTypeAnnotation(ts_type_annotation) => {
match &ts_type_annotation.type_annotation {
TSType::TSUnionType(union_type) => {
union_type.types.iter().for_each(|ts_type| {
if let TSType::TSTypeLiteral(literal) = ts_type {
let body = &literal.members;
check_member(&body[0], node, ctx);
}
});
}
TSType::TSTypeLiteral(literal) => {
let body = &literal.members;
if body.len() == 1 {
check_member(&body[0], node, ctx);
}
}
_ => {}
}
}
AstKind::TSTypeAliasDeclaration(ts_type_alias_decl) => {
match &ts_type_alias_decl.type_annotation {
TSType::TSUnionType(union_type) => {
union_type.types.iter().for_each(|ts_type| {
if let TSType::TSTypeLiteral(literal) = ts_type {
let body = &literal.members;
if body.len() == 1 {
check_member(&body[0], node, ctx);
}
}
});
}
TSType::TSIntersectionType(intersection_type) => {
intersection_type.types.iter().for_each(|ts_type| {
if let TSType::TSTypeLiteral(literal) = ts_type {
let body = &literal.members;
if body.len() == 1 {
check_member(&body[0], node, ctx);
}
}
});
}
TSType::TSTypeLiteral(literal) => {
let body = &literal.members;
if body.len() == 1 {
check_member(&body[0], node, ctx);
}
}
_ => {}
}
}
_ => {}
}
}
}
#[test]
fn test() {
use crate::tester::Tester;
let pass: Vec<&str> = vec![
"interface Foo { (): void; bar: number; }",
"type Foo = { (): void; bar: number; };",
"function foo(bar: { (): string; baz: number }): string { return bar(); }",
r"
interface Foo {
bar: string;
}
interface Bar extends Foo {
(): void;
}
",
r"
interface Foo {
bar: string;
}
interface Bar extends Function, Foo {
(): void;
}
",
];
let fail = vec![
"interface Foo { (): string; }",
"export default interface Foo { /** comment */ (): string; }",
r"
interface Foo {
// comment
(): string;
}
",
"export interface Foo { /** comment */ (): string; }",
r"
export interface Foo {
// comment
(): string;
}
",
r"
function foo(bar: { /* comment */ (s: string): number } | undefined): number {
return bar('hello');
}
",
r"
type Foo = {
(): string;
};
",
r"
function foo(bar: { (s: string): number }): number {
return bar('hello');
}
",
r"
function foo(bar: { (s: string): number } | undefined): number {
return bar('hello');
}
",
r"
interface Foo extends Function {
(): void;
}
",
r"
interface Foo<T> {
(bar: T): string;
}
",
r"
interface Foo<T> {
(this: T): void;
}
",
r"
type Foo<T> = { (this: string): T };
",
r"
interface Foo {
(arg: this): void;
}
",
r"
interface Foo {
(arg: number): this | undefined;
}
",
r"
// isn't actually valid ts but want to not give message saying it refers to Foo.
interface Foo {
(): {
a: {
nested: this;
};
between: this;
b: {
nested: string;
};
};
}
",
"type X = {} | { (): void; }",
"type X = {} & { (): void; };",
];
let fix = vec![
("interface Foo { (): string; }", "type Foo = () => string;", None),
(
r"
interface Foo {
// comment
(): string;
}
",
r"
// comment
type Foo = () => string;
",
None,
),
(
r"
interface Foo {
/* comment */
(): string;
}
",
r"
/* comment */
type Foo = () => string;
",
None,
),
(
r"
export interface Foo {
/** comment */
(): string;
}",
r"
/** comment */
export type Foo = () => string;",
None,
),
(
r"
export interface Foo {
// comment
(): string;
}
",
r"
// comment
export type Foo = () => string;
",
None,
),
(
r"
function foo(bar: { (s: string): number } | undefined): number {
return bar('hello');
}
",
r"
function foo(bar: ((s: string) => number) | undefined): number {
return bar('hello');
}
",
None,
),
(
r"
interface Foo extends Function {
(): void;
}
",
r"
type Foo = () => void;
",
None,
),
(
r"
interface Foo<T> {
(bar: T): string;
}
",
r"
type Foo<T> = (bar: T) => string;
",
None,
),
(
r"
type Foo = {
(): string;
};
",
r"
type Foo = () => string;
",
None,
),
(
r"
function foo(bar: { (s: string): number }): number {
return bar('hello');
}
",
r"
function foo(bar: (s: string) => number): number {
return bar('hello');
}
",
None,
),
(
r"
function foo(bar: { (s: string): number } | undefined): number {
return bar('hello');
}
",
r"
function foo(bar: ((s: string) => number) | undefined): number {
return bar('hello');
}
",
None,
),
(
r"
interface Foo extends Function {
(): void;
}
",
r"
type Foo = () => void;
",
None,
),
(
r"
interface Foo<T> {
(bar: T): string;
}
",
r"
type Foo<T> = (bar: T) => string;
",
None,
),
(
r"
interface Foo<T> {
(this: T): void;
}
",
r"
type Foo<T> = (this: T) => void;
",
None,
),
(
r"
type Foo<T> = { (this: string): T };
",
r"
type Foo<T> = (this: string) => T;
",
None,
),
(
r"
interface Foo {
(): {
a: {
nested: this;
};
between: this;
b: {
nested: string;
};
};
}
",
r"
type Foo = () => {
a: {
nested: this;
};
between: this;
b: {
nested: string;
};
};
",
None,
),
(
r"
type X = {} | { (): void; }
",
r"
type X = {} | (() => void)
",
None,
),
(
r"
type X = {} & { (): void; };
",
r"
type X = {} & (() => void);
",
None,
),
];
Tester::new(PreferFunctionType::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
}

View file

@ -0,0 +1,173 @@
---
source: crates/oxc_linter/src/tester.rs
assertion_line: 150
expression: prefer_function_type
---
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:1:17]
1 │ interface Foo { (): string; }
· ───────────
╰────
help: The function type form `() => string` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:1:47]
1 │ export default interface Foo { /** comment */ (): string; }
· ───────────
╰────
help: The function type form `() => string` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:4:11]
3 │ // comment
4 │ (): string;
· ───────────
5 │ }
╰────
help: The function type form `() => string` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:1:39]
1 │ export interface Foo { /** comment */ (): string; }
· ───────────
╰────
help: The function type form `() => string` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:4:11]
3 │ // comment
4 │ (): string;
· ───────────
5 │ }
╰────
help: The function type form `() => string` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:2:43]
1 │
2 │ function foo(bar: { /* comment */ (s: string): number } | undefined): number {
· ───────────────────
3 │ return bar('hello');
╰────
help: The function type form `(s: string) => number` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:3:11]
2 │ type Foo = {
3 │ (): string;
· ───────────
4 │ };
╰────
help: The function type form `() => string` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:2:29]
1 │
2 │ function foo(bar: { (s: string): number }): number {
· ───────────────────
3 │ return bar('hello');
╰────
help: The function type form `(s: string) => number` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:2:29]
1 │
2 │ function foo(bar: { (s: string): number } | undefined): number {
· ───────────────────
3 │ return bar('hello');
╰────
help: The function type form `(s: string) => number` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:3:11]
2 │ interface Foo extends Function {
3 │ (): void;
· ─────────
4 │ }
╰────
help: The function type form `() => void` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:3:11]
2 │ interface Foo<T> {
3 │ (bar: T): string;
· ─────────────────
4 │ }
╰────
help: The function type form `(bar: T) => string` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:3:11]
2 │ interface Foo<T> {
3 │ (this: T): void;
· ────────────────
4 │ }
╰────
help: The function type form `(this: T) => void` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:2:25]
1 │
2 │ type Foo<T> = { (this: string): T };
· ─────────────────
3 │
╰────
help: The function type form `(this: string) => T` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:3:11]
2 │ interface Foo {
3 │ (arg: this): void;
· ──────────────────
4 │ }
╰────
help: The function type form `(arg: this) => void` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:3:11]
2 │ interface Foo {
3 │ (arg: number): this | undefined;
· ────────────────────────────────
4 │ }
╰────
help: The function type form `(arg: number) => this | undefined` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:4:11]
3 │ interface Foo {
4 │ ╭─▶ (): {
5 │ │ a: {
6 │ │ nested: this;
7 │ │ };
8 │ │ between: this;
9 │ │ b: {
10 │ │ nested: string;
11 │ │ };
12 │ ╰─▶ };
13 │ }
╰────
help: The function type form `() => {
a: {
nested: this;
};
between: this;
b: {
nested: string;
};
}` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:1:17]
1 │ type X = {} | { (): void; }
· ─────────
╰────
help: The function type form `() => void` is generally preferred when possible for being more succinct.
⚠ typescript-eslint(prefer-function-type): Enforce using function types instead of interfaces with call signatures.
╭─[prefer_function_type.tsx:1:17]
1 │ type X = {} & { (): void; };
· ─────────
╰────
help: The function type form `() => void` is generally preferred when possible for being more succinct.