Skip to content

feat(assist): implement useSortedAttributes for HTML#9547

Merged
dyc3 merged 9 commits intobiomejs:nextfrom
mujpao:feat/use-sorted-attributes
Apr 5, 2026
Merged

feat(assist): implement useSortedAttributes for HTML#9547
dyc3 merged 9 commits intobiomejs:nextfrom
mujpao:feat/use-sorted-attributes

Conversation

@mujpao
Copy link
Copy Markdown

@mujpao mujpao commented Mar 19, 2026

Summary

Adds assist rule useSortedAttributes for HTML. Also refactors some of the code from the JSX version of useSortedAttributes to reduce code duplication.

Closes #9334

Test Plan

Snapshots tests in crates/biome_html_analyze/tests/specs/source/useSortedAttributes

Docs

There is documentation in crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 19, 2026

🦋 Changeset detected

Latest commit: 65094ee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@biomejs/biome Minor
@biomejs/cli-win32-x64 Minor
@biomejs/cli-win32-arm64 Minor
@biomejs/cli-darwin-x64 Minor
@biomejs/cli-darwin-arm64 Minor
@biomejs/cli-linux-x64 Minor
@biomejs/cli-linux-arm64 Minor
@biomejs/cli-linux-x64-musl Minor
@biomejs/cli-linux-arm64-musl Minor
@biomejs/wasm-web Minor
@biomejs/wasm-bundler Minor
@biomejs/wasm-nodejs Minor
@biomejs/backend-jsonrpc Patch
@biomejs/js-api Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added A-Linter Area: linter L-JavaScript Language: JavaScript and super languages L-HTML Language: HTML and super languages labels Mar 19, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a shared sorting abstraction: a public SortableAttribute trait and an AttributeGroup container for collecting and producing sorted attribute lists. Ports the existing JSX useSortedAttributes assist to HTML by introducing SortableHtmlAttribute and a new HTML assist UseSortedAttributes that groups sortable attributes, detects unsorted groups, and applies token-only replacements to sort them (supports natural and lexicographic orders). Refactors the JSX assist to use the shared abstractions (SortableJsxAttribute). Adds tests for HTML, Astro, Svelte and Vue, extends RuleSource with Svelte/Astro variants, adds a workspace dependency, and includes a changeset for a minor release.

Suggested reviewers

  • dyc3
  • ematipico
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: implementing the useSortedAttributes assist rule for HTML.
Description check ✅ Passed The description explains the implementation adds the useSortedAttributes assist for HTML and refactors JSX code to reduce duplication, with test plan and documentation references.
Linked Issues check ✅ Passed The PR successfully implements the objective from #9334: porting the useSortedAttributes assist action to HTML with support for Astro, Svelte, and Vue directives.
Out of Scope Changes check ✅ Passed All changes are scoped to the useSortedAttributes assist implementation for HTML. The refactoring of shared sorting logic and JSX updates directly support the main objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs (1)

102-105: Optional: Consider extracting comparator selection into a helper.

The comparator creation is duplicated between run() and action(). A small helper function could reduce this duplication.

♻️ Suggested helper function
fn get_comparator(
    sort_order: SortOrder,
) -> fn(&SortableHtmlAttribute, &SortableHtmlAttribute) -> Ordering {
    match sort_order {
        SortOrder::Natural => SortableHtmlAttribute::ascii_nat_cmp,
        SortOrder::Lexicographic => SortableHtmlAttribute::lexicographic_cmp,
    }
}

Then use get_comparator(options.sort_order.unwrap_or_default()) in both places.

Also applies to: 161-164

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs` around
lines 102 - 105, Extract the duplicated comparator selection into a small helper
function (e.g., get_comparator) that takes SortOrder and returns the comparator
fn(&SortableHtmlAttribute, &SortableHtmlAttribute) -> Ordering; replace the
match blocks in both run() and action() with calls to
get_comparator(options.sort_order.unwrap_or_default()) (or the appropriate
sort_order variable) so both places use the same helper and remove duplicated
match logic over SortOrder::Natural / SortOrder::Lexicographic mapping to
SortableHtmlAttribute::ascii_nat_cmp and ::lexicographic_cmp.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs`:
- Around line 102-105: Extract the duplicated comparator selection into a small
helper function (e.g., get_comparator) that takes SortOrder and returns the
comparator fn(&SortableHtmlAttribute, &SortableHtmlAttribute) -> Ordering;
replace the match blocks in both run() and action() with calls to
get_comparator(options.sort_order.unwrap_or_default()) (or the appropriate
sort_order variable) so both places use the same helper and remove duplicated
match logic over SortOrder::Natural / SortOrder::Lexicographic mapping to
SortableHtmlAttribute::ascii_nat_cmp and ::lexicographic_cmp.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aa4467ce-c90c-4f70-952c-330f8399b890

📥 Commits

Reviewing files that changed from the base of the PR and between 4d251d4 and 517e7d4.

⛔ Files ignored due to path filters (13)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/astro/sorted.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/astro/unsorted.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/sorted-lexicographic.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/sorted.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/unsorted-lexicographic.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/unsorted.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/sorted.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/unsorted.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/sorted.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (18)
  • .changeset/eleven-baths-wave.md
  • crates/biome_analyze/Cargo.toml
  • crates/biome_analyze/src/shared/mod.rs
  • crates/biome_analyze/src/shared/sort_attributes.rs
  • crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/astro/sorted.astro
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/astro/unsorted.astro
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/sorted-lexicographic.html
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/sorted-lexicographic.options.json
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/sorted.html
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/unsorted-lexicographic.html
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/unsorted-lexicographic.options.json
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/unsorted.html
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/sorted.svelte
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/unsorted.svelte
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/sorted.vue
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue
  • crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 19, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 168 skipped benchmarks1


Comparing mujpao:feat/use-sorted-attributes (65094ee) with next (ca5be09)

Open in CodSpeed

Footnotes

  1. 168 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Comment on lines +28 to +33
i Safe fix: Sort the HTML attributes.

1 │ - <p·v-text="msg"·v-show="ok"·dir="auto"·v-foo:bar.baz·id="hello"·class="flex"></p>
1 │ + <p·v-text="msg"·v-show="ok"·dir="auto"·v-foo:bar.baz·class="flex"id="hello"·></p>
2 2 │ <div v-slot:default v-on:click="doThis" v-once v-bind="{ id: someProp, 'other-attr': otherProp } "
3 3 │ v-bind:src="'/path/to/images/' + fileName"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we should discuss the order of vue directives. I personally prefer vue directives to generally be after all normal attributes, but there might be some prior art out there.

Comment on lines +112 to +121
9 9 │ onswiperight={prev}
10 10 │ {@attach myAttachment}
11 │ - ····spellcheck="true"
12 │ - ····tabindex="-1"
13 │ - ····dir="auto"
11 │ + ····dir="auto"
12 │ + ····spellcheck="true"
13 │ + ····tabindex="-1"
14 14 │ animate:flip
15 15 │ style:color="red"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same here, there might be prior art or popular conventions to consider here for svelte directives

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi! For prior art on Svelte directive ordering, the official eslint-plugin-svelte has a well-established sort-attributes rule with a default order that groups Svelte-specific directives by type.

It might make sense to align with this convention since it's what the Svelte ecosystem already uses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sounds good to me

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

What if the alphabetical sorting of HTML attributes conflicts with eslint-plugin-svelte's ordering? For instance, eslint-plugin-svelte's sort order puts id before class.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For now I would say let's ignore conflicting logic for base html attributes, and just focus on svelte specific directives

Comment on lines +8 to +17
client:load
class:list={classes}
set:text={text}
is:raw
dir="auto"
spellcheck="true"
tabindex="-1"
define:vars={vars}
server:defer
id="myid"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same for astro

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs`:
- Around line 145-151: text_range currently ignores the provided _state and
returns the whole element range, causing duplicate diagnostics; update
text_range(ctx: &RuleContext<Self>, state: &Self::State) to compute and return a
narrower TextRange covering only the current attribute group (use state.attrs:
get the first and last attribute nodes from state.attrs, compute their combined
span from first.range().start() to last.range().end(), and return that
TextRange) instead of the HtmlOpeningElement/HtmlSelfClosingElement
element.range(); this ensures the diagnostic is tied to the specific group
rather than the entire element.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d5a4de83-7c63-4acb-bc99-3a8bba45b1be

📥 Commits

Reviewing files that changed from the base of the PR and between 517e7d4 and 6cce591.

📒 Files selected for processing (1)
  • crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs

Comment on lines +145 to +151
fn text_range(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<TextRange> {
ctx.query().syntax().ancestors().skip(1).find_map(|node| {
HtmlOpeningElement::cast_ref(&node)
.map(|element| element.range())
.or_else(|| HtmlSelfClosingElement::cast_ref(&node).map(|element| element.range()))
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use the state-specific range to avoid duplicate diagnostics.

text_range currently ignores state, so two unsorted groups in the same element can emit duplicate diagnostics on the same element-wide span. Please narrow the range to the current group (for example, from first to last attribute in state.attrs).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs` around
lines 145 - 151, text_range currently ignores the provided _state and returns
the whole element range, causing duplicate diagnostics; update text_range(ctx:
&RuleContext<Self>, state: &Self::State) to compute and return a narrower
TextRange covering only the current attribute group (use state.attrs: get the
first and last attribute nodes from state.attrs, compute their combined span
from first.range().start() to last.range().end(), and return that TextRange)
instead of the HtmlOpeningElement/HtmlSelfClosingElement element.range(); this
ensures the diagnostic is tied to the specific group rather than the entire
element.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs (1)

211-215: Address TODO: trailing whitespace may be lost after sorting.

The replace_node_discard_trivia call could result in attributes merging together without separating whitespace. The commented-out logic suggests this is on your radar—worth tracking to ensure the fix is applied before release.

Would you like me to open an issue to track this, or do you plan to address it in this PR?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs` around
lines 211 - 215, The TODO warns that using
mutation.replace_node_discard_trivia(attr.clone(), sorted_attr) can drop
separating whitespace so ensure each sorted_attr keeps trailing whitespace
unless it's the group's last attribute: before calling
mutation.replace_node_discard_trivia, inspect
sorted_attr.syntax().last_trailing_trivia() and if None (and attr is not last in
group) append a whitespace trivia via sorted_attr =
sorted_attr.append_trivia_pieces([TriviaPiece::whitespace(1)]); then replace;
use the same check for attributes you generate to avoid merging tokens when
replacing nodes (refer to sorted_attr, TriviaPiece, append_trivia_pieces, attr,
and mutation.replace_node_discard_trivia).
crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs (1)

143-149: Same text_range concern as the HTML counterpart.

This function also ignores _state and returns the full element range. If an element has multiple unsorted groups (split by spreads), diagnostics may overlap on the same span. Consider narrowing to the group's range for consistency with the HTML fix (if applied).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs` around
lines 143 - 149, The text_range function currently ignores the rule State and
returns the full JSX element range, causing overlapping diagnostics when
multiple unsorted attribute groups (separated by spreads) exist; update
text_range to use the provided State (remove the underscore) and return the
TextRange for the specific attribute group that triggered the diagnostic instead
of the whole element. Concretely, inside text_range (and using
RuleContext<Self>), locate the same JsxOpeningElement/JsxSelfClosingElement as
now but then compute and return the contiguous group's range (e.g., derive from
the State passed into the rule which should contain the group's start/end/span
or compute it by iterating the element's attributes and detecting the span
between the group's first and last attribute, stopping at spread attributes), so
diagnostics are emitted only for that group's TextRange rather than
element.range().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs`:
- Around line 211-215: The TODO warns that using
mutation.replace_node_discard_trivia(attr.clone(), sorted_attr) can drop
separating whitespace so ensure each sorted_attr keeps trailing whitespace
unless it's the group's last attribute: before calling
mutation.replace_node_discard_trivia, inspect
sorted_attr.syntax().last_trailing_trivia() and if None (and attr is not last in
group) append a whitespace trivia via sorted_attr =
sorted_attr.append_trivia_pieces([TriviaPiece::whitespace(1)]); then replace;
use the same check for attributes you generate to avoid merging tokens when
replacing nodes (refer to sorted_attr, TriviaPiece, append_trivia_pieces, attr,
and mutation.replace_node_discard_trivia).

In `@crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs`:
- Around line 143-149: The text_range function currently ignores the rule State
and returns the full JSX element range, causing overlapping diagnostics when
multiple unsorted attribute groups (separated by spreads) exist; update
text_range to use the provided State (remove the underscore) and return the
TextRange for the specific attribute group that triggered the diagnostic instead
of the whole element. Concretely, inside text_range (and using
RuleContext<Self>), locate the same JsxOpeningElement/JsxSelfClosingElement as
now but then compute and return the contiguous group's range (e.g., derive from
the State passed into the rule which should contain the group's start/end/span
or compute it by iterating the element's attributes and detecting the span
between the group's first and last attribute, stopping at spread attributes), so
diagnostics are emitted only for that group's TextRange rather than
element.range().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a16ae846-9370-44ba-ab8d-f18026d545fa

📥 Commits

Reviewing files that changed from the base of the PR and between 6cce591 and e2af7cf.

⛔ Files ignored due to path filters (9)
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/astro/sorted.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/astro/unsorted.astro.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/sorted.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/unsorted.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/sorted.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/unsorted.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/sorted.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (12)
  • crates/biome_analyze/src/shared/sort_attributes.rs
  • crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/astro/sorted.astro
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/astro/unsorted.astro
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/sorted.html
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/unsorted.html
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/sorted.svelte
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/unsorted.svelte
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/sorted.vue
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue
  • crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs
  • crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx
✅ Files skipped from review due to trivial changes (6)
  • crates/biome_js_analyze/tests/specs/source/useSortedAttributes/unsorted.jsx
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/astro/unsorted.astro
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/unsorted.svelte
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/sorted.vue
  • crates/biome_analyze/src/shared/sort_attributes.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/astro/sorted.astro
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/sorted.svelte

@mujpao
Copy link
Copy Markdown
Author

mujpao commented Apr 1, 2026

I've added sorting for the Astro, Svelte, and Vue directives.
There's a bug in the code action that I'm having trouble fixing where, if the last attribute in the tag needs to be moved and is not followed by any white space, the sorted code ends up with attributes that have no spaces between them. For example, <input type="checkbox" name="" id="" checked> will become <input checkedid="" name="" type="checkbox">.
I can't figure out how to add a whitespace token to the end of the attribute.

for (SortableHtmlAttribute(attr), SortableHtmlAttribute(sorted_attr)) in
zip(state.attrs.iter(), state.get_sorted_attributes(comparator))
{
// TODO make sure sorted_attr has trailing whitespace if it is not the last attribute in the group
// if sorted_attr.syntax().last_trailing_trivia().is_none() {
// sorted_attr = sorted_attr.append_trivia_pieces([TriviaPiece::whitespace(1)]);
// }
mutation.replace_node_discard_trivia(attr.clone(), sorted_attr);
}

@mujpao
Copy link
Copy Markdown
Author

mujpao commented Apr 1, 2026

Should new rule sources be added for eslint-plugin-astro and eslint-plugin-svelte?

sources: &[RuleSource::HtmlEslint("sort-attrs").inspired(), RuleSource::EslintVueJs("attributes-order").inspired()],

@dyc3
Copy link
Copy Markdown
Contributor

dyc3 commented Apr 1, 2026

Should new rule sources be added for eslint-plugin-astro and eslint-plugin-svelte?

sure, that would make sense

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs (1)

193-199: ⚠️ Potential issue | 🟡 Minor

Use the current group for the diagnostic span.

run() can return more than one AttributeGroup for a single tag, but text_range() still reports the whole element. If one element has two unsorted groups split by {...spread} or v-bind="object", you’ll emit duplicate diagnostics on the same range.

🩹 Suggested fix
-    fn text_range(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<TextRange> {
-        ctx.query().syntax().ancestors().skip(1).find_map(|node| {
-            HtmlOpeningElement::cast_ref(&node)
-                .map(|element| element.range())
-                .or_else(|| HtmlSelfClosingElement::cast_ref(&node).map(|element| element.range()))
-        })
+    fn text_range(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<TextRange> {
+        let first = state.attrs.first()?.0.range();
+        let last = state.attrs.last()?.0.range();
+        Some(TextRange::new(first.start(), last.end()))
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs` around
lines 193 - 199, text_range currently finds the element ancestor and returns the
whole element range, causing duplicate diagnostics when run() yields multiple
AttributeGroup ranges; change text_range to prefer the current node from the
query (ctx.query().current()) and, if that node is an HtmlAttributeGroup (or
whatever AttributeGroup type your rule yields), return that group's range,
falling back to the HtmlOpeningElement/HtmlSelfClosingElement element.range()
only if no current AttributeGroup is found; update references in text_range to
check AttributeGroup::cast_ref(&current_node) first, then the existing element
checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs`:
- Around line 235-293: The SortableHtmlAttribute::category currently buckets
every plain attribute (except "ref") as HtmlAttribute which leaves Vue
directives like "v-if" or ":" categorized incorrectly; update
SortableHtmlAttribute::category to inspect the attribute name (from
attr.name().and_then(...).map(|t| t.text_trimmed())) and if it matches Vue
patterns map them to the appropriate SortCategory (e.g. exact "v-for" ->
SortCategory::VueListRendering, "v-if"/"v-else-if"/"v-else" -> VueConditional,
"v-model" -> VueTwoWayBinding, names starting with "v-" -> VueOtherAttribute,
names starting with ":" -> VueOtherAttribute (v-bind), names starting with "@"
or "v-on" -> VueEvent, "v-slot" -> VueSlot, and keep "ref" -> VueUnique) instead
of falling through to HtmlAttribute; make this change inside the
SortableHtmlAttribute::category match arm for AnyHtmlAttribute::HtmlAttribute so
Vue-specific attributes are categorized before defaulting to
SortCategory::HtmlAttribute.

---

Duplicate comments:
In `@crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs`:
- Around line 193-199: text_range currently finds the element ancestor and
returns the whole element range, causing duplicate diagnostics when run() yields
multiple AttributeGroup ranges; change text_range to prefer the current node
from the query (ctx.query().current()) and, if that node is an
HtmlAttributeGroup (or whatever AttributeGroup type your rule yields), return
that group's range, falling back to the
HtmlOpeningElement/HtmlSelfClosingElement element.range() only if no current
AttributeGroup is found; update references in text_range to check
AttributeGroup::cast_ref(&current_node) first, then the existing element checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ab1499c9-beb7-4ef4-8c25-44eafd1a350f

📥 Commits

Reviewing files that changed from the base of the PR and between e2af7cf and 9120d75.

⛔ Files ignored due to path filters (4)
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/unsorted-lexicographic.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/unsorted.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/svelte/unsorted.svelte.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (6)
  • crates/biome_analyze/src/rule.rs
  • crates/biome_analyze/src/shared/sort_attributes.rs
  • crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/html/unsorted.html
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue
  • crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/biome_html_analyze/tests/specs/source/useSortedAttributes/vue/unsorted.vue

Comment on lines +235 to +293
#[derive(PartialEq, Eq, Clone, PartialOrd, Ord)]
enum SortCategory {
HtmlAttribute,
AstroClassDirective,
AstroClientDirective,
AstroDefineDirective,
AstroIsDirective,
AstroServerDirective,
AstroSetDirective,
SvelteBindThisDirective,
SvelteStyleDirective,
SvelteClassDirective,
SvelteBindDirective,
SvelteUseDirective,
SvelteTransitionDirective,
SvelteInDirective,
SvelteOutDirective,
SvelteAnimateDirective,
SvelteAttachAttribute,

VueDefinition,
VueListRendering,
VueConditional,
VueRenderModifier,
VueUnique,
VueSlot,
VueTwoWayBinding,
VueCustomDirective,
// `v-bind`, etc.
VueOtherAttribute,
VueEvent,
VueContent,

Unknown,
}

#[derive(PartialEq, Eq, Clone)]
pub struct SortableHtmlAttribute(AnyHtmlAttribute);

impl SortableHtmlAttribute {
fn category(&self) -> SortCategory {
match &self.0 {
AnyHtmlAttribute::HtmlAttribute(attr) => {
if let Ok(attr_name) = attr
.name()
.and_then(|name| name.value_token())
.as_ref()
.map(|token| token.text_trimmed())
{
match attr_name {
// Vue ref attribute
"ref" => SortCategory::VueUnique,
_ => SortCategory::HtmlAttribute,
}
} else {
SortCategory::HtmlAttribute
}
}
AnyHtmlAttribute::HtmlAttributeSingleTextExpression(_) => SortCategory::HtmlAttribute,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Vue templates are still being sorted as “HTML first”.

Every plain attribute except ref is bucketed as HtmlAttribute, and that bucket sits before all the Vue-specific categories. In a .vue file, mixed cases like <div v-if="ok" id="x"> will therefore be normalised with id before v-if, so the new assist does not actually apply a full Vue ordering yet. A tiny Vue fixture mixing a plain attribute with v-if/v-for would lock this down once fixed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/assist/source/use_sorted_attributes.rs` around
lines 235 - 293, The SortableHtmlAttribute::category currently buckets every
plain attribute (except "ref") as HtmlAttribute which leaves Vue directives like
"v-if" or ":" categorized incorrectly; update SortableHtmlAttribute::category to
inspect the attribute name (from attr.name().and_then(...).map(|t|
t.text_trimmed())) and if it matches Vue patterns map them to the appropriate
SortCategory (e.g. exact "v-for" -> SortCategory::VueListRendering,
"v-if"/"v-else-if"/"v-else" -> VueConditional, "v-model" -> VueTwoWayBinding,
names starting with "v-" -> VueOtherAttribute, names starting with ":" ->
VueOtherAttribute (v-bind), names starting with "@" or "v-on" -> VueEvent,
"v-slot" -> VueSlot, and keep "ref" -> VueUnique) instead of falling through to
HtmlAttribute; make this change inside the SortableHtmlAttribute::category match
arm for AnyHtmlAttribute::HtmlAttribute so Vue-specific attributes are
categorized before defaulting to SortCategory::HtmlAttribute.

Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

Looks good, just need to resolve the merge conflict

@Netail
Copy link
Copy Markdown
Member

Netail commented Apr 5, 2026

gtg @dyc3

@dyc3 dyc3 merged commit 01f8473 into biomejs:next Apr 5, 2026
33 checks passed
@mujpao mujpao deleted the feat/use-sorted-attributes branch April 5, 2026 23:54
@coderabbitai coderabbitai bot mentioned this pull request Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter L-HTML Language: HTML and super languages L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants