diff --git a/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/sorted.vue.snap b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/sorted.vue.snap
new file mode 100644
index 000000000000..89e047800b24
--- /dev/null
+++ b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/sorted.vue.snap
@@ -0,0 +1,71 @@
+---
+source: crates/biome_html_analyze/tests/spec_tests.rs
+expression: sorted.vue
+---
+# Input
+```vue
+
+
+```
diff --git a/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue
new file mode 100644
index 000000000000..7e817071aec9
--- /dev/null
+++ b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue
@@ -0,0 +1,63 @@
+
diff --git a/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue.snap b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue.snap
new file mode 100644
index 000000000000..71b30efeb9c3
--- /dev/null
+++ b/crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue.snap
@@ -0,0 +1,472 @@
+---
+source: crates/biome_html_analyze/tests/spec_tests.rs
+expression: unsorted.vue
+---
+# Input
+```vue
+
+
+```
+
+# Diagnostics
+```
+unsorted.vue:1:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ > 1 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 2 │
+ 3 │
+ 1 │ +
+ 2 2 │
+ 3 3 │
+ 2 │
+ > 3 │
4 │ v-slot:default
+ ...
+ > 19 │ :class="{ red: isRed }"
+ > 20 │ >
+ │ ^
+ 21 │
+ 22 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 2 2 │
+ 3 3 │
+ 21 21 │
+
+
+```
+
+```
+unsorted.vue:23:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 22 │
+ > 23 │
24 │ v-slot:default
+ ...
+ > 40 │ :class="{ red: isRed }"
+ > 41 │ >
+ │ ^
+ 42 │
+ 43 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 22 22 │
+ 23 23 │
+ > 23 │
24 │ v-slot:default
+ ...
+ > 40 │ :class="{ red: isRed }"
+ > 41 │ >
+ │ ^
+ 42 │
+ 43 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 27 27 │ ref="my-ref"
+ 28 28 │ v-bind="{ id: someProp, 'other-attr': otherProp }"
+ 29 │ - ··:key="item.id"
+ 30 │ - ··v-bind:src="'/path/to/images/'·+·fileName"
+ 31 │ - ··spellcheck="true"
+ 32 │ - ··v-for="item·in·items"
+ 33 │ - ··tabindex="-1"
+ 34 │ - ··@scroll.passive="onScroll"
+ 35 │ - ··:is="tabs[currentTab]"
+ 36 │ - ··v-text="msg"
+ 37 │ - ··dir="auto"
+ 38 │ - ··v-model="countModel"
+ 39 │ - ··v-if="awesome"
+ 40 │ - ··:class="{·red:·isRed·}"
+ 29 │ + ··dir="auto"
+ 30 │ + ··spellcheck="true"
+ 31 │ + ··tabindex="-1"
+ 32 │ + ··:is="tabs[currentTab]"
+ 33 │ + ··v-for="item·in·items"
+ 34 │ + ··v-if="awesome"
+ 35 │ + ··:key="item.id"
+ 36 │ + ··v-model="countModel"
+ 37 │ + ··:class="{·red:·isRed·}"
+ 38 │ + ··v-bind:src="'/path/to/images/'·+·fileName"
+ 39 │ + ··@scroll.passive="onScroll"
+ 40 │ + ··v-text="msg"
+ 41 41 │ >
+ 42 42 │
+
+
+```
+
+```
+unsorted.vue:44:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 43 │
+ > 44 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 45 │
+ 46 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 42 42 │
+ 43 43 │
+ 44 │ -
+ 44 │ +
+ 45 45 │
+ 46 46 │
+
+
+```
+
+```
+unsorted.vue:47:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 45 │
+ 46 │
+ > 47 │
+ │ ^^^^^^^^^^^^^^^^^^
+ 48 │
+ 49 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 45 45 │
+ 46 46 │
+ 47 │ -
+ 47 │ +
+ 48 48 │
+ 49 49 │
+
+
+```
+
+```
+unsorted.vue:49:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 47 │
+ 48 │
+ > 49 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 50 │
+ 51 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 47 47 │
+ 48 48 │
+ 49 │ -
+ 49 │ +
+ 50 50 │
+ 51 51 │
+
+
+```
+
+```
+unsorted.vue:51:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 49 │
+ 50 │
+ > 51 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 52 │
+ 53 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 49 49 │
+ 50 50 │
+ 51 │ -
+ 51 │ +
+ 52 52 │
+ 53 53 │
+
+
+```
+
+```
+unsorted.vue:53:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 51 │
+ 52 │
+ > 53 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 54 │
+ 55 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 51 51 │
+ 52 52 │
+ 53 │ -
+ 53 │ +
+ 54 54 │
+ 55 55 │
+
+
+```
+
+```
+unsorted.vue:55:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 53 │
+ 54 │
+ > 55 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 56 │
+ 57 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 53 53 │
+ 54 54 │
+ 55 │ -
+ 55 │ +
+ 56 56 │
+ 57 57 │
+
+
+```
+
+```
+unsorted.vue:57:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 55 │
+ 56 │
+ > 57 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 58 │
+ 59 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 55 55 │
+ 56 56 │
+ 57 │ -
+ 57 │ +
+ 58 58 │
+ 59 59 │
+
+
+```
+
+```
+unsorted.vue:59:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 57 │
+ 58 │
+ > 59 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 60 │
+ 61 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 57 57 │
+ 58 58 │
+ 59 │ -
+ 59 │ +
+ 60 60 │
+ 61 61 │
+
+
+```
+
+```
+unsorted.vue:61:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 59 │
+ 60 │
+ > 61 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 62 │
+ 63 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 59 59 │
+ 60 60 │
+ 61 │ -
+ 61 │ +
+ 62 62 │
+ 63 63 │
+
+
+```
+
+```
+unsorted.vue:63:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 61 │
+ 62 │
+ > 63 │
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 64 │
+
+ i Safe fix: Sort the HTML attributes.
+
+ 61 61 │
+ 62 62 │
+ 63 │ -
+ 63 │ +
+ 64 64 │
+
+
+```
diff --git a/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs b/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs
index f81f0c984e60..781d50fd23e1 100644
--- a/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs
+++ b/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs
@@ -1,5 +1,6 @@
use std::{borrow::Cow, cmp::Ordering, iter::zip};
+use biome_analyze::shared::sort_attributes::{AttributeGroup, SortableAttribute};
use biome_analyze::{
Ast, FixKind, Rule, RuleAction, RuleDiagnostic, RuleSource, context::RuleContext,
declare_source_rule,
@@ -8,11 +9,11 @@ use biome_console::markup;
use biome_deserialize::TextRange;
use biome_diagnostics::{Applicability, category};
use biome_js_syntax::{
- AnyJsxAttribute, JsxAttribute, JsxAttributeList, JsxOpeningElement, JsxSelfClosingElement,
+ AnyJsxAttribute, JsLanguage, JsxAttribute, JsxAttributeList, JsxOpeningElement,
+ JsxSelfClosingElement,
};
-use biome_rowan::{AstNode, BatchMutationExt};
+use biome_rowan::{AstNode, AstNodeExt, BatchMutationExt, SyntaxToken};
use biome_rule_options::use_sorted_attributes::{SortOrder, UseSortedAttributesOptions};
-use biome_string_case::StrLikeExtension;
use crate::JsRuleAction;
@@ -82,30 +83,31 @@ declare_source_rule! {
impl Rule for UseSortedAttributes {
type Query = Ast
;
- type State = PropGroup;
+ type State = AttributeGroup;
type Signals = Box<[Self::State]>;
type Options = UseSortedAttributesOptions;
fn run(ctx: &RuleContext) -> Self::Signals {
let props = ctx.query();
- let mut current_prop_group = PropGroup::default();
+ let mut current_prop_group = AttributeGroup::default();
let mut prop_groups = Vec::new();
let options = ctx.options();
let sort_by = options.sort_order.unwrap_or_default();
let comparator = match sort_by {
- SortOrder::Natural => PropElement::ascii_nat_cmp,
- SortOrder::Lexicographic => PropElement::lexicographic_cmp,
+ SortOrder::Natural => SortableJsxAttribute::ascii_nat_cmp,
+ SortOrder::Lexicographic => SortableJsxAttribute::lexicographic_cmp,
};
// Convert to boolean-based comparator for is_sorted_by
- let boolean_comparator =
- |a: &PropElement, b: &PropElement| comparator(a, b) != Ordering::Greater;
+ let boolean_comparator = |a: &SortableJsxAttribute, b: &SortableJsxAttribute| {
+ comparator(a, b) != Ordering::Greater
+ };
for prop in props {
match prop {
AnyJsxAttribute::JsxAttribute(attr) => {
- current_prop_group.props.push(PropElement { prop: attr });
+ current_prop_group.attrs.push(SortableJsxAttribute(attr));
}
// spread prop reset sort order
AnyJsxAttribute::JsxSpreadAttribute(_) => {
@@ -113,7 +115,7 @@ impl Rule for UseSortedAttributes {
&& !current_prop_group.is_sorted(boolean_comparator)
{
prop_groups.push(current_prop_group);
- current_prop_group = PropGroup::default();
+ current_prop_group = AttributeGroup::default();
} else {
// Reuse the same buffer
current_prop_group.clear();
@@ -152,14 +154,14 @@ impl Rule for UseSortedAttributes {
let sort_by = options.sort_order.unwrap_or_default();
let comparator = match sort_by {
- SortOrder::Natural => PropElement::ascii_nat_cmp,
- SortOrder::Lexicographic => PropElement::lexicographic_cmp,
+ SortOrder::Natural => SortableJsxAttribute::ascii_nat_cmp,
+ SortOrder::Lexicographic => SortableJsxAttribute::lexicographic_cmp,
};
- for (PropElement { prop }, PropElement { prop: sorted_prop }) in
- zip(state.props.iter(), state.get_sorted_props(comparator))
+ for (SortableJsxAttribute(attr), SortableJsxAttribute(sorted_attr)) in
+ zip(state.attrs.iter(), state.get_sorted_attributes(comparator)?)
{
- mutation.replace_node_discard_trivia(prop.clone(), sorted_prop);
+ mutation.replace_node_discard_trivia(attr.clone(), sorted_attr);
}
Some(RuleAction::new(
@@ -172,65 +174,30 @@ impl Rule for UseSortedAttributes {
}
#[derive(PartialEq, Eq, Clone)]
-pub struct PropElement {
- prop: JsxAttribute,
-}
+pub struct SortableJsxAttribute(JsxAttribute);
-impl PropElement {
- pub fn ascii_nat_cmp(&self, other: &Self) -> Ordering {
- let (Ok(self_name), Ok(other_name)) = (self.prop.name(), other.prop.name()) else {
- return Ordering::Equal;
- };
- let (Ok(self_name), Ok(other_name)) = (self_name.name(), other_name.name()) else {
- return Ordering::Equal;
- };
+impl SortableAttribute for SortableJsxAttribute {
+ type Language = JsLanguage;
- self_name
- .text_trimmed()
- .ascii_nat_cmp(other_name.text_trimmed())
+ fn name(&self) -> Option> {
+ self.0.name().ok()?.name_token().ok()
}
- pub fn lexicographic_cmp(&self, other: &Self) -> Ordering {
- let (Ok(self_name), Ok(other_name)) = (self.prop.name(), other.prop.name()) else {
- return Ordering::Equal;
- };
- let (Ok(self_name), Ok(other_name)) = (self_name.name(), other_name.name()) else {
- return Ordering::Equal;
- };
-
- self_name
- .text_trimmed()
- .lexicographic_cmp(other_name.text_trimmed())
+ fn node(&self) -> &impl AstNode {
+ &self.0
}
-}
-
-#[derive(Clone, Default)]
-pub struct PropGroup {
- props: Vec,
-}
-impl PropGroup {
- fn is_empty(&self) -> bool {
- self.props.is_empty()
- }
-
- fn is_sorted(&self, comparator: F) -> bool
+ fn replace_token(
+ self,
+ prev_token: SyntaxToken,
+ next_token: SyntaxToken,
+ ) -> Option
where
- F: Fn(&PropElement, &PropElement) -> bool,
+ Self: Sized,
{
- self.props.is_sorted_by(comparator)
- }
-
- fn get_sorted_props(&self, comparator: F) -> Vec
- where
- F: FnMut(&PropElement, &PropElement) -> Ordering,
- {
- let mut new_props = self.props.clone();
- new_props.sort_by(comparator);
- new_props
- }
-
- fn clear(&mut self) {
- self.props.clear();
+ Some(Self(
+ self.0
+ .replace_token_discard_trivia(prev_token, next_token)?,
+ ))
}
}
diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx
index 51cb93063441..fe3021c594bc 100644
--- a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx
+++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx
@@ -4,3 +4,10 @@
/>;
;
;
+
+{/* ; */}
+
+;
+
+{/* ; */}
+
diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx.snap b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx.snap
index c28055390519..0601de0b3577 100644
--- a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx.snap
+++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx.snap
@@ -11,6 +11,13 @@ expression: unsorted.jsx
;
;
+{/* ; */}
+
+;
+
+{/* ; */}
+
+
```
# Diagnostics
@@ -30,13 +37,13 @@ unsorted.jsx:1:1 assist/source/useSortedAttributes FIXABLE ━━━━━━
i Safe fix: Sort the JSX props.
- 1 1 │ ;
- 5 5 │ ;
+ 1 1 │ ;
+ 5 5 │ ;
```
@@ -55,12 +62,12 @@ unsorted.jsx:5:1 assist/source/useSortedAttributes FIXABLE ━━━━━━
i Safe fix: Sort the JSX props.
- 3 3 │ firstName="John"
- 4 4 │ />;
- 5 │ - ;
- 5 │ + ;
- 6 6 │ ;
- 7 7 │
+ 3 3 │ firstName="John"
+ 4 4 │ />;
+ 5 │ - ;
+ 5 │ + ;
+ 6 6 │ ;
+ 7 7 │
```
@@ -79,12 +86,12 @@ unsorted.jsx:5:1 assist/source/useSortedAttributes FIXABLE ━━━━━━
i Safe fix: Sort the JSX props.
- 3 3 │ firstName="John"
- 4 4 │ />;
- 5 │ - ;
- 5 │ + ;
- 6 6 │ ;
- 7 7 │
+ 3 3 │ firstName="John"
+ 4 4 │ />;
+ 5 │ - ;
+ 5 │ + ;
+ 6 6 │ ;
+ 7 7 │
```
@@ -99,14 +106,40 @@ unsorted.jsx:6:1 assist/source/useSortedAttributes FIXABLE ━━━━━━
> 6 │ ;
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 │
+ 8 │ {/* ; */}
+
+ i Safe fix: Sort the JSX props.
+
+ 4 4 │ />;
+ 5 5 │ ;
+ 6 │ - ;
+ 6 │ + ;
+ 7 7 │
+ 8 8 │ {/* ; */}
+
+
+```
+
+```
+unsorted.jsx:10:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i The attributes are not sorted.
+
+ 8 │ {/* ; */}
+ 9 │
+ > 10 │ ;
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 11 │
+ 12 │ {/* ; */}
i Safe fix: Sort the JSX props.
- 4 4 │ />;
- 5 5 │ ;
- 6 │ - ;
- 6 │ + ;
- 7 7 │
+ 8 8 │ {/* ; */}
+ 9 9 │
+ 10 │ - ;
+ 10 │ + ;
+ 11 11 │
+ 12 12 │ {/* ; */}
```
diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
index 8a75b3572760..d19e56613753 100644
--- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts
+++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
@@ -1029,7 +1029,7 @@ See https://biomejs.dev/assist/actions/organize-imports
*/
recommended?: boolean;
/**
- * Enforce attribute sorting in JSX elements.
+ * Enforce attribute sorting in HTML elements.
See https://biomejs.dev/assist/actions/use-sorted-attributes
*/
useSortedAttributes?: UseSortedAttributesConfiguration;
diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json
index 9637094a68a2..38dc9155aae3 100644
--- a/packages/@biomejs/biome/configuration_schema.json
+++ b/packages/@biomejs/biome/configuration_schema.json
@@ -11946,7 +11946,7 @@
"type": ["boolean", "null"]
},
"useSortedAttributes": {
- "description": "Enforce attribute sorting in JSX elements.\nSee https://biomejs.dev/assist/actions/use-sorted-attributes",
+ "description": "Enforce attribute sorting in HTML elements.\nSee https://biomejs.dev/assist/actions/use-sorted-attributes",
"anyOf": [
{ "$ref": "#/$defs/UseSortedAttributesConfiguration" },
{ "type": "null" }