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_analyzer): useJsxSortProps #2652

Merged
merged 6 commits into from
Aug 28, 2024

Conversation

vohoanglong0107
Copy link
Contributor

@vohoanglong0107 vohoanglong0107 commented Apr 30, 2024

Summary

Implement Eslint's lint rule [jsx-sort-props](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-sort-props.md] as an assist

Test Plan

All tests should pass

@github-actions github-actions bot added A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels Apr 30, 2024
Copy link

codspeed-hq bot commented Apr 30, 2024

CodSpeed Performance Report

Merging #2652 will not alter performance

Comparing vohoanglong0107:feat-use-jsx-sort-props (49ad1ed) with main (88f1d5e)

Summary

✅ 101 untouched benchmarks

@github-actions github-actions bot added the A-CLI Area: CLI label Apr 30, 2024
@vohoanglong0107 vohoanglong0107 marked this pull request as ready for review April 30, 2024 13:41
@ematipico
Copy link
Member

I'm not very fond of having stylistic rules inside our linter. I think this rule should be an assist.

I was working on a similar rule for sorting JSON keys, and setting up the foundations for assists inside the CLI and LSP.

@vohoanglong0107
Copy link
Contributor Author

Oh ok, then I will set this PR to draft for the time being

@vohoanglong0107 vohoanglong0107 marked this pull request as draft May 1, 2024 08:10
@ematipico
Copy link
Member

Hey @vohoanglong0107, the infrastructure is ready. You should move the new rule inside src/assists/source, and use the macro declare_source_rule

@PrinOrange
Copy link

This rule will be helpful if it will be passed.

@github-actions github-actions bot removed A-Project Area: project A-Diagnostic Area: diagnostocis labels Aug 15, 2024
@vohoanglong0107
Copy link
Contributor Author

@ematipico How may I generate the configuration and the schema for an assist rule? just new-js-assistrule doesn't seem to work, the same goes for just gen-all 😥

@github-actions github-actions bot added the A-Project Area: project label Aug 21, 2024
@vohoanglong0107 vohoanglong0107 marked this pull request as ready for review August 21, 2024 08:06
/// <Hello tel={5555555} {...this.props} firstName="John" lastName="Smith" />;
/// ```
///
/// ## Options
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have options anymore, so we should remove the docs too


fn action(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<JsRuleAction> {
let props = ctx.query().clone();
let mut non_spread_props: Option<Vec<_>> = None;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option<Vec<>> is not very easy to use. You can just use Vec<>, and if the vector is empty, it means it's like None

Comment on lines 16 to 21
/// Enforce props sorting in JSX elements.
///
/// This rule checks if the JSX props are sorted in a consistent way.
/// A spread prop resets the sorting order.
///
/// The rule can be configured to sort props alphabetically, ignore case, and more.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no more configuration, however we should document very well what's the order, meaning which kinds props come first and which ones come later.

Comment on lines 132 to 65
AnyJsxAttribute::JsxSpreadAttribute(_) => {
if let Some(mut non_spread_props) = non_spread_props.take() {
non_spread_props.sort_by(compare_props());
new_props.extend(
non_spread_props
.into_iter()
.map(AnyJsxAttribute::JsxAttribute),
);
}
non_spread_props = None;
new_props.push(prop);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not really sure I understand this part. Why do we run a sorting every time we encounter a spread prop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that, given that a spread props resets the sorting order, every time we encounter a spread prop, we sort the previous non-spread props element.

Copy link
Member

@ematipico ematipico Aug 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, I believe the same approach we use in the organise imports would work. It makes things easier, I think.

Essentially, you create groups. Each group is delimited by a spread prop, where the spread prop is always the last one of the group.

Each group is sorted in alphabetical order (I think natural order is better), and groups aren't sorted, they are placed as they were found.

return None;
}
let mut mutation = ctx.root().begin();
mutation.replace_node_discard_trivia(props, jsx_attribute_list(new_props));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we discard the trivia?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we shouldn't. My bad

Comment on lines 169 to 107
fn compare_props() -> impl FnMut(&JsxAttribute, &JsxAttribute) -> Ordering {
|a: &JsxAttribute, b: &JsxAttribute| -> Ordering {
let (Ok(a_name), Ok(b_name)) = (a.name(), b.name()) else {
return Ordering::Equal;
};
let (a_name, b_name) = (a_name.text(), b_name.text());

a_name.cmp(&b_name)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should take advantage of the Rust system. Rust has Ord and PartialOrd directives. Which means that the sorting will come for free once you implement the function.

Check how we do the sorting in this rule: https://github.com/biomejs/biome/blob/main/crates/biome_json_analyze/src/assists/source/use_sorted_keys.rs

We implement the ordering here:

impl Ord for MemberKey {
fn cmp(&self, other: &Self) -> Ordering {
// Sort keys using natural ordering
natord::compare(
&self.node.name().unwrap().text(),
&other.node.name().unwrap().text(),
)
}
}

Then, we use a is_sorted function to check if we need to run to emit an action or not:

if !state.is_sorted() {
Some(state)
} else {
None
}

Generally, this approach is better than the one you implemented because in this case, you always run the action, while in this case, we always run the action.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I used a separate function for comparison was that, originally, I wanted to customize it with user options, and Ord doesn't support that (?). Even though this PR implements a trimmed down version without the options, I think it still makes sense to keep this in case we want to reintroduce the option later.

However, if we aren't going to support options for assist rules anytime soon, I would follow your guidance and change it to use Ord

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why this wouldn't work with options. You just create a new type that holds a reference to the options other than the nodes to sort.

When implementing Ord and PartialOrd, you will read the options and change the logic accordingly

Comment on lines +53 to +69
let props = ctx.query().clone();
let mut current_prop_group = PropGroup::default();
let mut prop_groups = Vec::new();
for prop in props.clone() {
match prop {
AnyJsxAttribute::JsxAttribute(attr) => {
current_prop_group.props.push(PropElement { prop: attr });
}
// spread prop reset sort order
AnyJsxAttribute::JsxSpreadAttribute(_) => {
prop_groups.push(current_prop_group);
current_prop_group = PropGroup::default();
}
}
}
prop_groups.push(current_prop_group);
prop_groups
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really looks cleaner after being split to groups

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! We're almost there, we just need some final touch for the docs, then we can merge it

///
/// This rule checks if the JSX props are sorted in a consistent way.
/// Props are sorted alphabetically.
/// A spread prop resets the sorting order.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This phrase is not very beginner-friendly. What does "resetting the sorting order" mean? I would suggest something more extensive:

Suggested change
/// A spread prop resets the sorting order.
/// When the rule encounters a spread prop, the algorithm stops ordering all the previous props. The prevents from breaking the override of certain props that rely on the order.

Please review the suggestion, and make sure that I didn't suggest anything wrong.

Comment on lines +25 to +27
/// ```js,expect_diagnostic
/// <Hello lastName="Smith" firstName="John" />;
/// ```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It definitely makes sense to add an example with spread props, so users can understand how it works. Maybe two spread props should be enough

@vohoanglong0107
Copy link
Contributor Author

@ematipico How do you feel about the new docs?

@ematipico
Copy link
Member

They're good, thank you!

@ematipico ematipico merged commit 3542e9a into biomejs:main Aug 28, 2024
12 checks passed
@vohoanglong0107 vohoanglong0107 deleted the feat-use-jsx-sort-props branch August 31, 2024 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-CLI Area: CLI A-Linter Area: linter A-Project Area: project L-JavaScript Language: JavaScript and super languages
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants