Skip to content

Commit

Permalink
feat(biome_js_analyze): useShorthandFunctionType
Browse files Browse the repository at this point in the history
  • Loading branch information
Eddy Brown committed Nov 5, 2023
1 parent 1a457c7 commit 0c1bbb1
Show file tree
Hide file tree
Showing 19 changed files with 524 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ define_categories! {
"lint/nursery/useGroupedTypeImport": "https://biomejs.dev/linter/rules/use-grouped-type-import",
"lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions",
"lint/nursery/useShorthandAssign": "https://biomejs.dev/lint/rules/use-shorthand-assign",
"lint/nursery/useShorthandFunctionType": "https://biomejs.dev/lint/rules/use-shorthand-function-type",
"lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread",
"lint/performance/noDelete": "https://biomejs.dev/linter/rules/no-delete",
"lint/security/noDangerouslySetInnerHtml": "https://biomejs.dev/linter/rules/no-dangerously-set-inner-html",
Expand Down
1 change: 1 addition & 0 deletions crates/biome_js_analyze/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ biome_json_syntax = { workspace = true }
biome_rowan = { workspace = true }
bpaf.workspace = true
lazy_static = { workspace = true }
log = "0.4.20"
natord = "1.0.9"
roaring = "0.10.1"
rustc-hash = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/analyzers/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use crate::JsRuleAction;
use biome_analyze::{
context::RuleContext, declare_rule, ActionCategory, Ast, FixKind, Rule, RuleDiagnostic,
};
use biome_console::markup;
use biome_diagnostics::Applicability;
use biome_js_factory::make;
use biome_js_factory::make::ts_type_alias_declaration;
use biome_js_syntax::AnyTsType::TsThisType;
use biome_js_syntax::{
AnyJsDeclarationClause, AnyTsReturnType, AnyTsType, TsCallSignatureTypeMember, TsFunctionType,
TsInterfaceDeclaration, TsObjectType, TsTypeMemberList, T,
};
use biome_rowan::{AstNode, AstNodeList, BatchMutationExt, TriviaPieceKind};

declare_rule! {
/// Enforce using function types instead of object type 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.
///
/// Source: https://typescript-eslint.io/rules/prefer-function-type/
///
/// ## Examples
///
/// ### Invalid
///
/// ```ts,expect_diagnostic
/// interface Example {
/// (): string;
/// }
/// ```
///
/// ```ts,expect_diagnostic
/// function foo(example: { (): number }): number {
/// return example();
/// }
/// ```
///
/// ## Valid
///
/// ```ts
/// type Example = () => string;
/// ```
///
/// ```ts
/// function foo(example: () => number): number {
/// return bar();
/// }
/// ```
///
/// ```ts
/// // returns the function itself, not the `this` argument.
/// type ReturnsSelf2 = (arg: string) => ReturnsSelf;
/// ```
///
/// ```ts
/// interface Foo {
/// bar: string;
/// }
/// interface Bar extends Foo {
/// (): void;
/// }
/// ```
///
/// ```ts
/// // multiple call signatures (overloads) is allowed:
/// interface Overloaded {
/// (data: string): number;
/// (id: number): string;
/// }
/// // this is equivelent to Overloaded interface.
/// type Intersection = ((data: string) => number) & ((id: number) => string);
///```
///
pub(crate) UseShorthandFunctionType {
version: "1.3.0",
name: "useShorthandFunctionType",
recommended: false,
fix_kind: FixKind::Safe,
}
}

impl Rule for UseShorthandFunctionType {
type Query = Ast<TsCallSignatureTypeMember>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let query = ctx.query();

if let Some(ts_type_member_list) = query.parent::<TsTypeMemberList>() {
// If there is more than one member, it's not a single call signature.
if ts_type_member_list.len() > 1 {
return None;
}
// If the parent is an interface with an extends clause, it's not a single call signature.
if let Some(interface_decl) = ts_type_member_list.parent::<TsInterfaceDeclaration>() {
if interface_decl.extends_clause().is_some() {
return None;
}

if let AnyTsReturnType::AnyTsType(TsThisType(_)) =
query.return_type_annotation()?.ty().ok()?
{
return None;
}
}
return Some(());
}

None
}

fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
Some(RuleDiagnostic::new(rule_category!(), ctx.query().range(), markup! {
"Use a function type instead of a call signature."
}).note(markup! { "Types containing only a call signature can be shortened to a function type." }))
}

fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
let node = ctx.query();
let mut mutation = ctx.root().begin();

let ts_type_member_list = node.parent::<TsTypeMemberList>()?;

if let Some(interface_decl) = ts_type_member_list.parent::<TsInterfaceDeclaration>() {
let type_alias_declaration = ts_type_alias_declaration(
make::token(T![type]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
interface_decl.id().ok()?,
make::token(T![=]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
AnyTsType::from(convert_ts_call_signature_type_member_to_function_type(
node,
)?),
)
.build();

mutation.replace_node(
AnyJsDeclarationClause::from(interface_decl),
AnyJsDeclarationClause::from(type_alias_declaration),
);

return Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::Always,
message: markup! { "Convert empty interface to type alias." }.to_owned(),
mutation,
});
}

if let Some(ts_object_type) = ts_type_member_list.parent::<TsObjectType>() {
let new_function_type = convert_ts_call_signature_type_member_to_function_type(node)?;

mutation.replace_node(
AnyTsType::from(ts_object_type),
AnyTsType::from(new_function_type),
);

return Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::Always,
message: markup! { "Convert object type to type alias." }.to_owned(),
mutation,
});
}

None
}
}

fn convert_ts_call_signature_type_member_to_function_type(
node: &TsCallSignatureTypeMember,
) -> Option<TsFunctionType> {
let new_node = make::ts_function_type(
make::js_parameters(
make::token(T!['(']),
node.parameters().ok()?.items(),
make::token(T![')']).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
),
make::token(T![=>]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
node.return_type_annotation()?.ty().ok()?,
)
.build();

Some(new_node.with_type_parameters(node.type_parameters()))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface Example {
(): string;
}

function foo(example: { (): number }): number {
return example();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid.ts
---
# Input
```js
interface Example {
(): string;
}

function foo(example: { (): number }): number {
return example();
}
```

# Diagnostics
```
invalid.ts:2:2 lint/nursery/useShorthandFunctionType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Use a function type instead of a call signature.
1 │ interface Example {
> 2 │ (): string;
│ ^^^^^^^^^^^
3 │ }
4 │
i Types containing only a call signature can be shortened to a function type.
i Safe fix: Convert empty interface to type alias.
1 │ - interface·Example·{
2 │ - ·():·string;
3 │ - }
1 │ + type·Example·=·()·=>·string
4 2 │
5 3 │ function foo(example: { (): number }): number {
```

```
invalid.ts:5:25 lint/nursery/useShorthandFunctionType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Use a function type instead of a call signature.
3 │ }
4 │
> 5 │ function foo(example: { (): number }): number {
│ ^^^^^^^^^^
6 │ return example();
7 │ }
i Types containing only a call signature can be shortened to a function type.
i Safe fix: Convert object type to type alias.
3 3 │ }
4 4 │
5 │ - function·foo(example:·{·():·number·}):·number·{
5 │ + function·foo(example:·()·=>·number):·number·{
6 6 │ return example();
7 7 │ }
```


Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type Example = () => string;

function foo(example: () => number): number {
return bar();
}

// returns the function itself, not the `this` argument.
type ReturnsSelf = (arg: string) => ReturnsSelf;

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 equivelent to Overloaded interface.
type Intersection = ((data: string) => number) & ((id: number) => string);

interface ReturnsSelf {
// returns the function itself, not the `this` argument.
(arg: string): this;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: valid.ts
---
# Input
```js
type Example = () => string;

function foo(example: () => number): number {
return bar();
}

// returns the function itself, not the `this` argument.
type ReturnsSelf = (arg: string) => ReturnsSelf;

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 equivelent to Overloaded interface.
type Intersection = ((data: string) => number) & ((id: number) => string);

interface ReturnsSelf {
// returns the function itself, not the `this` argument.
(arg: string): this;
}
```


Loading

0 comments on commit 0c1bbb1

Please sign in to comment.