-
-
Notifications
You must be signed in to change notification settings - Fork 504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(biome_js_analyze): useShorthandFunctionType
#670
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking this! It is not an easy rule!
I suggested some change. Don't hesitate to ask more details if you need them :)
crates/biome_js_analyze/src/analyzers/nursery/use_shorthand_function_type.rs
Show resolved
Hide resolved
} | ||
|
||
impl Rule for UseShorthandFunctionType { | ||
type Query = Semantic<Query>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can directly query TsCallSignatureTypeMember
?
This could save unnecessary lookup :)
Moreover, we can use Ast
instead of Semantic
. Semantic
is required when you use the semantic model ctx.model()
.
type Query = Semantic<Query>; | |
type Query = Ast<TsCallSignatureTypeMember>; |
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)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think we can use the same diagnostic for interface and object type. What do you think?
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)) | |
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." })) |
|
||
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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we change the query to TsCallSignatureTypeMember
, we have to verify that it is the only member of the type.
Taking a look at js.ungramar
, We note that TsCallSignatureTypeMember
is part of the union AnyTsMember
, AnyTsMember
is only used in TsTypeMemberList
. Thus, we could just retrieve the list and check that there is only one entry.
Note that here I assumed that State = ()
.
By the way, I think you are right about your concern on returning this. We should verify that the call signature doesn't return this
. If it returns this
, then we ignore the code.
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 | |
} | |
} | |
(query.parent::<TsTypeMemberList>()?.len() == 1).then_some(()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow this is much more simple! I'll need to add some more logic to cover a couple of edge cases.
Some(RuleDiagnostic::new(rule_category!(), range, message).note(note)) | ||
} | ||
|
||
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I let you rework this part with the new suggested changes :)
Brilliant, thanks for the comments. I'll have a look at this later today hopefully! |
useShorthandFunctionType
@Conaclos I've just pushed up a new (and hopefully improved!) version. |
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, | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to use match
here, but I ran into an issue where the expected parent type was AnyJsStatement
so I couldn't also compare to the TsObjectType
which is the parent when the function signature is inside of the object type element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is ok :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay.
I think we are almost there :)
I think it lacks a last tthing: examples with comments.
// Comment
interface Example {
(): string;
}
// Comment
type F = { (): number } // Trailing comment
Also, we should not suggest a fix when a comment is removed. I think it is the case for examples for inner comments:
interface Example2 {
// Inner comment
(): string; // Inner trailing comment
}
type G = {
// Inner comment
(): number // Inner trailing comment
}
You can check if a node have comments with something like: node.syntax().has_comments()
.
crates/biome_js_analyze/src/analyzers/nursery/use_shorthand_function_type.rs
Outdated
Show resolved
Hide resolved
crates/biome_js_analyze/src/analyzers/nursery/use_shorthand_function_type.rs
Outdated
Show resolved
Hide resolved
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, | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is ok :)
@emab gentle bump, are you still interested in this? I can follow if you are busy :) |
Oops, didn't see the comment. I'll tidy this up and get it ready when I can! |
✅ Deploy Preview for rad-torte-839a59 ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
Thanks for taking this over, unfortunately I've had very little time 😞 |
Summary
lint/useShorthandFunctionType
-typescript-eslint/prefer-function-type
#48WIP
Test Plan