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 4, 2023
1 parent c6dda82 commit f6536e1
Show file tree
Hide file tree
Showing 15 changed files with 576 additions and 4 deletions.
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 @@ -110,6 +110,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
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,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)
}
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;
}
Loading

0 comments on commit f6536e1

Please sign in to comment.