Skip to content
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

Merged
merged 6 commits into from
Dec 4, 2023
Merged

Conversation

emab
Copy link
Contributor

@emab emab commented Nov 4, 2023

@github-actions github-actions bot added A-Project Area: project A-Linter Area: linter A-Website Area: website L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels Nov 4, 2023
Copy link
Member

@Conaclos Conaclos left a 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 :)

}

impl Rule for UseShorthandFunctionType {
type Query = Semantic<Query>;
Copy link
Member

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().

Suggested change
type Query = Semantic<Query>;
type Query = Ast<TsCallSignatureTypeMember>;

Comment on lines 154 to 170
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))
Copy link
Member

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?

Suggested change
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." }))

Comment on lines 113 to 117

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
}
}
Copy link
Member

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.

Suggested change
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(())

Copy link
Contributor Author

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> {
Copy link
Member

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 :)

@emab
Copy link
Contributor Author

emab commented Nov 5, 2023

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 :)

Brilliant, thanks for the comments. I'll have a look at this later today hopefully!

@emab emab changed the title feat(biome_js_analyze): useShorthandFunctionType feat(biome_js_analyze): useShorthandFunctionType Nov 5, 2023
@emab
Copy link
Contributor Author

emab commented Nov 5, 2023

@Conaclos I've just pushed up a new (and hopefully improved!) version.

Comment on lines 134 to 172
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,
});
}
Copy link
Contributor Author

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.

Copy link
Member

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 :)

Copy link
Member

@Conaclos Conaclos left a 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().

Comment on lines 134 to 172
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,
});
}
Copy link
Member

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 :)

@unvalley
Copy link
Member

@emab gentle bump, are you still interested in this? I can follow if you are busy :)

@emab
Copy link
Contributor Author

emab commented Nov 25, 2023

Oops, didn't see the comment. I'll tidy this up and get it ready when I can!

Copy link

netlify bot commented Dec 4, 2023

Deploy Preview for rad-torte-839a59 ready!

Name Link
🔨 Latest commit e7a18f7
🔍 Latest deploy log https://app.netlify.com/sites/rad-torte-839a59/deploys/656e0fb123d6e7000912051f
😎 Deploy Preview https://deploy-preview-670--rad-torte-839a59.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@ematipico ematipico merged commit 81761f8 into biomejs:main Dec 4, 2023
11 of 13 checks passed
@emab
Copy link
Contributor Author

emab commented Dec 5, 2023

Thanks for taking this over, unfortunately I've had very little time 😞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Changelog Area: changelog A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Project Area: project A-Website Area: website L-JavaScript Language: JavaScript and super languages
Projects
None yet
Development

Successfully merging this pull request may close these issues.

📎 Implement lint/useShorthandFunctionType - typescript-eslint/prefer-function-type
4 participants