-
-
Notifications
You must be signed in to change notification settings - Fork 504
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(biome_js_analyze): useShorthandFunctionType
- Loading branch information
Eddy Brown
committed
Nov 4, 2023
1 parent
c6dda82
commit f6536e1
Showing
15 changed files
with
576 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
276 changes: 276 additions & 0 deletions
276
crates/biome_js_analyze/src/analyzers/nursery/use_shorthand_function_type.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
use crate::semantic_services::Semantic; | ||
use crate::JsRuleAction; | ||
use biome_analyze::{ | ||
context::RuleContext, declare_rule, ActionCategory, Rule, RuleAction, RuleDiagnostic, | ||
}; | ||
use biome_console::markup; | ||
use biome_diagnostics::Applicability; | ||
use biome_js_factory::make; | ||
use biome_js_syntax::{AnyJsDeclarationClause, AnyTsReturnType, AnyTsType, JsParameterList, JsFormalParameter, JsParameters, JsSyntaxToken, TsFunctionType, TsInterfaceDeclaration, TsTypeAliasDeclaration, T, TsTypeParameters, TsIdentifierBinding, AnyJsFormalParameter, AnyJsExpression}; | ||
use biome_js_syntax::AnyTsType::TsObjectType; | ||
use biome_js_syntax::AnyTsTypeMember::TsCallSignatureTypeMember; | ||
use biome_js_syntax::parameter_ext::AnyParameter::AnyJsParameter; | ||
use biome_rowan::{ | ||
declare_node_union, AstNode, AstNodeExt, AstNodeList, BatchMutationExt, SyntaxResult, | ||
TextRange, TriviaPieceKind, | ||
}; | ||
|
||
declare_rule! { | ||
/// 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(); | ||
/// } | ||
/// ``` | ||
/// | ||
/// ```ts,expect_diagnostic | ||
/// interface ReturnsSelf { | ||
/// // returns the function itself, not the `this` argument. | ||
/// (arg: string): this; | ||
/// } | ||
/// ``` | ||
/// | ||
/// ## 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, | ||
} | ||
} | ||
|
||
declare_node_union! { | ||
pub(crate) Query = TsInterfaceDeclaration | JsFormalParameter | ||
} | ||
|
||
pub(crate) enum RuleState { | ||
TsInterfaceDeclaration(TextRange, JsParameterList, SyntaxResult<AnyTsReturnType>), | ||
TsObjectType(TextRange, JsParameterList, SyntaxResult<AnyTsReturnType>), | ||
} | ||
|
||
impl Rule for UseShorthandFunctionType { | ||
type Query = Semantic<Query>; | ||
type State = RuleState; | ||
type Signals = Option<Self::State>; | ||
type Options = (); | ||
|
||
fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
let query = ctx.query(); | ||
|
||
match query { | ||
Query::TsInterfaceDeclaration(interface_declaration) => { | ||
if interface_declaration.members().len() == 1 | ||
&& interface_declaration.extends_clause().is_none() | ||
{ | ||
if let Some(TsCallSignatureTypeMember(call_signature)) = | ||
interface_declaration.members().first() | ||
{ | ||
return Some(RuleState::TsInterfaceDeclaration( | ||
query.range(), | ||
call_signature.parameters().ok()?.items(), | ||
call_signature.return_type_annotation()?.ty(), | ||
)); | ||
} | ||
} | ||
None | ||
} | ||
Query::JsFormalParameter(parameter) => { | ||
if let Some(TsObjectType(ts_object)) = parameter.type_annotation()?.ty().ok() { | ||
if ts_object.members().len() == 1 { | ||
if let Some(ts_call_signature_type_member) = ts_object | ||
.members() | ||
.first()? | ||
.as_ts_call_signature_type_member() | ||
{ | ||
return Some(RuleState::TsObjectType( | ||
ts_object.range(), | ||
ts_call_signature_type_member.parameters().ok()?.items(), | ||
ts_call_signature_type_member | ||
.return_type_annotation()?.ty(), | ||
)); | ||
} | ||
} | ||
} | ||
None | ||
} | ||
} | ||
} | ||
|
||
fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { | ||
let (range, message, note) = match state { | ||
RuleState::TsInterfaceDeclaration(range, parameters, return_type_syntax) => ( | ||
range, | ||
markup! { | ||
"Prefer function type over interface." | ||
}, | ||
markup! {"Interface only has a call signature, you should use a function type instead."}, | ||
), | ||
RuleState::TsObjectType(range, parameters, return_type_syntax) => ( | ||
range, | ||
markup! { | ||
"Prefer function type over object type." | ||
}, | ||
markup! {"Object only has a call signature, you should use a function type instead."}, | ||
), | ||
}; | ||
Some(RuleDiagnostic::new(rule_category!(), range, message).note(note)) | ||
} | ||
|
||
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> { | ||
let node = ctx.query(); | ||
let mut mutation = ctx.root().begin(); | ||
|
||
match node { | ||
Query::TsInterfaceDeclaration(interface_declaration) => { | ||
let ts_call_signature_type_member = interface_declaration.members().first()?.as_ts_call_signature_type_member()?.clone(); | ||
|
||
let new_type_alias = create_type_alias( | ||
interface_declaration.id().ok()?, | ||
interface_declaration.type_parameters(), | ||
AnyTsType::from(make::ts_function_type( | ||
make::js_parameters( | ||
make::token(T!['(']), | ||
ts_call_signature_type_member.parameters().ok()?.items(), | ||
make::token(T![')']).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), | ||
), | ||
make::token(T![=>]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), | ||
ts_call_signature_type_member.return_type_annotation()?.ty().ok()?, | ||
).build()) | ||
); | ||
|
||
mutation.replace_node( | ||
AnyJsDeclarationClause::from(interface_declaration.clone()), | ||
AnyJsDeclarationClause::from(new_type_alias?), | ||
); | ||
|
||
return Some(JsRuleAction { | ||
category: ActionCategory::QuickFix, | ||
applicability: Applicability::Always, | ||
message: markup! { "Convert empty interface to type alias." }.to_owned(), | ||
mutation, | ||
}); | ||
} | ||
Query::JsFormalParameter(formal_parameter) => { | ||
let ts_call_signature_type_member = formal_parameter.type_annotation()?.ty().ok()?.as_ts_object_type()?.members().first()?.as_ts_call_signature_type_member()?.clone(); | ||
|
||
let function_type = make::ts_function_type( | ||
make::js_parameters( | ||
make::token(T!['(']), | ||
ts_call_signature_type_member.parameters().ok()?.items(), | ||
make::token(T![')']).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), | ||
), | ||
make::token(T![=>]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), | ||
ts_call_signature_type_member.return_type_annotation()?.ty().ok()?, | ||
).build(); | ||
|
||
let new_type_annotation = make::ts_type_annotation( | ||
make::token(T![:]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), | ||
AnyTsType::from(function_type), | ||
); | ||
|
||
mutation.replace_node( | ||
formal_parameter.type_annotation()?, | ||
new_type_annotation, | ||
); | ||
|
||
return Some(JsRuleAction { | ||
category: ActionCategory::QuickFix, | ||
applicability: Applicability::Always, | ||
message: markup! { "Convert object type signature to function type." }.to_owned(), | ||
mutation, | ||
}); | ||
}, | ||
} | ||
} | ||
} | ||
fn create_type_alias( | ||
id: TsIdentifierBinding, | ||
type_params: Option<TsTypeParameters>, | ||
ts_type: AnyTsType, | ||
) -> Option<TsTypeAliasDeclaration> { | ||
let new_node = make::ts_type_alias_declaration( | ||
make::token(T![type]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), | ||
id, | ||
make::token(T![=]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), | ||
ts_type, | ||
); | ||
|
||
let new_node = if type_params.is_some() { | ||
new_node.with_type_parameters(type_params?).build() | ||
} else { | ||
new_node.build() | ||
}; | ||
|
||
Some(new_node) | ||
} | ||
|
||
fn create_function_type_parameter( | ||
type_params: JsParameterList, | ||
ts_type: AnyTsType, | ||
) -> Option<TsFunctionType> { | ||
let new_node = make::ts_function_type( | ||
make::js_parameters( | ||
make::token(T!['(']), | ||
type_params, | ||
make::token(T![')']).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), | ||
), | ||
make::token(T![=>]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), | ||
AnyTsReturnType::AnyTsType(ts_type), | ||
).build(); | ||
|
||
Some(new_node) | ||
} |
12 changes: 12 additions & 0 deletions
12
crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/invalid.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
interface Example { | ||
(): string; | ||
} | ||
|
||
function foo(example: { (): number }): number { | ||
return example(); | ||
} | ||
|
||
interface ReturnsSelf { | ||
// returns the function itself, not the `this` argument. | ||
(arg: string): this; | ||
} |
Oops, something went wrong.