Skip to content

Commit

Permalink
feat(biome_js_analyzer): useJsxSortProps
Browse files Browse the repository at this point in the history
  • Loading branch information
vohoanglong0107 committed Apr 30, 2024
1 parent 1abda0c commit c2cf7a7
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 8 deletions.
23 changes: 21 additions & 2 deletions crates/biome_configuration/src/linter/rules.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ define_categories! {
"lint/correctness/useValidForDirection": "https://biomejs.dev/linter/rules/use-valid-for-direction",
"lint/correctness/useYield": "https://biomejs.dev/linter/rules/use-yield",
"lint/nursery/colorNoInvalidHex": "https://biomejs.dev/linter/rules/color-no-invalid-hex",
"lint/nursery/useArrayLiterals": "https://biomejs.dev/linter/rules/use-array-literals",
"lint/nursery/noColorInvalidHex": "https://biomejs.dev/linter/rules/no-color-invalid-hex",
"lint/nursery/noConsole": "https://biomejs.dev/linter/rules/no-console",
"lint/nursery/noConstantMathMinMaxClamp": "https://biomejs.dev/linter/rules/no-constant-math-min-max-clamp",
Expand All @@ -130,13 +129,15 @@ define_categories! {
"lint/nursery/noRestrictedImports": "https://biomejs.dev/linter/rules/no-restricted-imports",
"lint/nursery/noTypeOnlyImportAttributes": "https://biomejs.dev/linter/rules/no-type-only-import-attributes",
"lint/nursery/noUndeclaredDependencies": "https://biomejs.dev/linter/rules/no-undeclared-dependencies",
"lint/nursery/noUselessUndefinedInitialization": "https://biomejs.dev/linter/rules/no-useless-undefined-initialization",
"lint/nursery/noUnknownUnit": "https://biomejs.dev/linter/rules/no-unknown-unit",
"lint/nursery/noUselessUndefinedInitialization": "https://biomejs.dev/linter/rules/no-useless-undefined-initialization",
"lint/nursery/useArrayLiterals": "https://biomejs.dev/linter/rules/use-array-literals",
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
"lint/nursery/useConsistentNewBuiltin": "https://biomejs.dev/linter/rules/use-consistent-new-builtin",
"lint/nursery/useGenericFontNames": "https://biomejs.dev/linter/rules/use-generic-font-names",
"lint/nursery/useDefaultSwitchClause": "https://biomejs.dev/linter/rules/use-default-switch-clause",
"lint/nursery/useGenericFontNames": "https://biomejs.dev/linter/rules/use-generic-font-names",
"lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions",
"lint/nursery/useJsxSortProps": "https://biomejs.dev/linter/rules/use-jsx-sort-props",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
"lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread",
"lint/performance/noBarrelFile": "https://biomejs.dev/linter/rules/no-barrel-file",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

235 changes: 235 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/use_jsx_sort_props.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
use std::cmp::Ordering;

use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic};
use biome_console::markup;
use biome_deserialize_macros::Deserializable;
use biome_js_syntax::{
AnyJsxAttribute, AnyJsxTag, JsxAttribute, JsxAttributeList, JsxTagExpression,
};
use biome_rowan::{AstNode, TextRange};
use serde::{Deserialize, Serialize};

declare_rule! {
/// Succinct description of the rule.
///
/// Put context and details about the rule.
/// As a starting point, you can take the description of the corresponding _ESLint_ rule (if any).
///
/// Try to stay consistent with the descriptions of implemented rules.
///
/// Add a link to the corresponding ESLint rule (if any):
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// var a = 1;
/// a = 2;
/// ```
///
/// ### Valid
///
/// ```js
/// // var a = 1;
/// ```
///
pub UseJsxSortProps {
version: "next",
name: "useJsxSortProps",
recommended: false,
}
}

#[derive(Clone, Debug, Default, Deserializable, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UseJsxSortPropsOptions {
#[serde(default, skip_serializing_if = "is_default")]
callbacks_last: bool,
shorthand_first: bool,
shorthand_last: bool,
multiline: MultilineBehavior,
ignore_case: bool,
no_sort_alphabetically: bool,
// TODO: add reserved_first and locale options
}

#[derive(Clone, Debug, Default, Deserializable, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum MultilineBehavior {
#[default]
Ignore,
First,
Last,
}

pub enum UseJsxSortPropsState {
Alphabetic,
CallbacksLast,
ShorthandFirst,
ShorthandLast,
MultilineFirst,
MultilineLast,
}

fn is_default<T: Default + Eq>(value: &T) -> bool {
value == &T::default()
}

impl Rule for UseJsxSortProps {
type Query = Ast<JsxTagExpression>;
type State = TextRange;
type Signals = Vec<Self::State>;
type Options = UseJsxSortPropsOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let options = ctx.options();
match node.tag() {
Ok(AnyJsxTag::JsxElement(tag)) => {
let Ok(opening) = tag.opening_element() else {
return Vec::new();
};
let attrs = opening.attributes();
lint_props(attrs, options)
}
Ok(AnyJsxTag::JsxSelfClosingElement(closing)) => {
let attrs = closing.attributes();
lint_props(attrs, options)
}
_ => Vec::new(),
}
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(RuleDiagnostic::new(
rule_category!(),
*state,
markup! {
"These JSX props should be sorted."
},
))
}
}

fn lint_props(props: JsxAttributeList, options: &UseJsxSortPropsOptions) -> Vec<TextRange> {
let mut diagnostics = vec![];
let mut non_spread_props: Option<Vec<_>> = None;
for prop in props {
match prop {
AnyJsxAttribute::JsxAttribute(attr) => {
if let Some(non_spread_props) = &mut non_spread_props {
non_spread_props.push(attr);
} else {
non_spread_props = Some(vec![attr]);
}
}
AnyJsxAttribute::JsxSpreadAttribute(_) => {
if let Some(non_spread_props) = non_spread_props.take() {
diagnostics.extend(lint_non_spread_props(&non_spread_props, options));
}
non_spread_props = None;
}
}
}
if let Some(non_spread_props) = non_spread_props {
diagnostics.extend(lint_non_spread_props(&non_spread_props, options));
}
diagnostics
}

fn lint_non_spread_props(
props: &[JsxAttribute],
options: &UseJsxSortPropsOptions,
) -> Option<TextRange> {
let mut sorted_props = props.to_vec();
sorted_props.sort_by(compare_props(options));
for (i, prop) in props.iter().enumerate() {
if prop.name().ok()?.text() != sorted_props[i].name().ok()?.text() {
return Some(TextRange::new(
props.first()?.range().start(),
props.last()?.range().end(),
));
}
}
None
}

fn compare_props(
options: &UseJsxSortPropsOptions,
) -> 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());

if options.callbacks_last {
if is_callback(a) && !is_callback(b) {
return Ordering::Greater;
}
if !is_callback(a) && is_callback(b) {
return Ordering::Less;
}
}

if options.shorthand_first {
if is_shorthand(a) && !is_shorthand(b) {
return Ordering::Less;
}
if !is_shorthand(a) && is_shorthand(b) {
return Ordering::Greater;
}
}

if options.shorthand_last {
if is_shorthand(a) && !is_shorthand(b) {
return Ordering::Greater;
}
if !is_shorthand(a) && is_shorthand(b) {
return Ordering::Less;
}
}

if options.multiline == MultilineBehavior::First {
if is_multiline(a) && !is_multiline(b) {
return Ordering::Less;
}
if !is_multiline(a) && is_multiline(b) {
return Ordering::Greater;
}
}

if options.multiline == MultilineBehavior::Last {
if is_multiline(a) && !is_multiline(b) {
return Ordering::Greater;
}
if !is_multiline(a) && is_multiline(b) {
return Ordering::Less;
}
}

if options.no_sort_alphabetically {
return Ordering::Equal;
}

if options.ignore_case {
return a_name.to_lowercase().cmp(&b_name.to_lowercase());
}
a_name.cmp(&b_name)
}
}

fn is_shorthand(prop: &JsxAttribute) -> bool {
prop.initializer().is_some()
}

fn is_callback(prop: &JsxAttribute) -> bool {
prop.name().is_ok_and(|name| name.text().starts_with("on"))
}

fn is_multiline(prop: &JsxAttribute) -> bool {
prop.text().contains('\n')
}
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ pub type UseImportType =
pub type UseIsArray = <lint::suspicious::use_is_array::UseIsArray as biome_analyze::Rule>::Options;
pub type UseIsNan = <lint::correctness::use_is_nan::UseIsNan as biome_analyze::Rule>::Options;
pub type UseJsxKeyInIterable = < lint :: correctness :: use_jsx_key_in_iterable :: UseJsxKeyInIterable as biome_analyze :: Rule > :: Options ;
pub type UseJsxSortProps =
<lint::nursery::use_jsx_sort_props::UseJsxSortProps as biome_analyze::Rule>::Options;
pub type UseKeyWithClickEvents =
<lint::a11y::use_key_with_click_events::UseKeyWithClickEvents as biome_analyze::Rule>::Options;
pub type UseKeyWithMouseEvents =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* should not generate diagnostics */
<Hello firstName="John" lastName="Smith" />;
<Hello tel={5555555} {...this.props} firstName="John" lastName="Smith" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: sorted.jsx
---
# Input
```jsx
/* should not generate diagnostics */
<Hello firstName="John" lastName="Smith" />;
<Hello tel={5555555} {...this.props} firstName="John" lastName="Smith" />;

```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Hello lastName="Smith" firstName="John" />;
Loading

0 comments on commit c2cf7a7

Please sign in to comment.