-
-
Notifications
You must be signed in to change notification settings - Fork 489
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
feat(biome_js_analyzer): useJsxSortProps #2652
Conversation
c2cf7a7
to
886494a
Compare
CodSpeed Performance ReportMerging #2652 will not alter performanceComparing Summary
|
886494a
to
0d48b11
Compare
870e6bf
to
d90a4b6
Compare
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. |
Oh ok, then I will set this PR to draft for the time being |
Hey @vohoanglong0107, the infrastructure is ready. You should move the new rule inside |
This rule will be helpful if it will be passed. |
d90a4b6
to
dececc2
Compare
@ematipico How may I generate the configuration and the schema for an assist rule? |
bb08a91
to
8d7f015
Compare
/// <Hello tel={5555555} {...this.props} firstName="John" lastName="Smith" />; | ||
/// ``` | ||
/// | ||
/// ## Options |
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.
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; |
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.
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
/// 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. |
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.
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.
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); | ||
} |
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 am not really sure I understand this part. Why do we run a sorting every time we encounter a spread prop?
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.
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.
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.
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)); |
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.
Why do we discard the trivia?
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.
Yeah we shouldn't. My bad
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) | ||
} | ||
} |
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.
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:
biome/crates/biome_json_analyze/src/assists/source/use_sorted_keys.rs
Lines 29 to 37 in d079e53
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:
biome/crates/biome_json_analyze/src/assists/source/use_sorted_keys.rs
Lines 105 to 109 in d079e53
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.
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.
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
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 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
9671138
to
666b19e
Compare
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 |
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.
This really looks cleaner after being split to groups
Vec::is_empty() is equivalent to None
Separated by spread props
5e97240
to
53af54c
Compare
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.
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. |
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.
This phrase is not very beginner-friendly. What does "resetting the sorting order" mean? I would suggest something more extensive:
/// 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.
/// ```js,expect_diagnostic | ||
/// <Hello lastName="Smith" firstName="John" />; | ||
/// ``` |
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.
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
@ematipico How do you feel about the new docs? |
They're good, thank you! |
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