Skip to content

feat(assist): implement useSortedAttributes for HTML#9547

Open
mujpao wants to merge 2 commits intobiomejs:nextfrom
mujpao:feat/use-sorted-attributes
Open

feat(assist): implement useSortedAttributes for HTML#9547
mujpao wants to merge 2 commits intobiomejs:nextfrom
mujpao:feat/use-sorted-attributes

Conversation

@mujpao
Copy link

@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

changeset-bot bot commented Mar 19, 2026

🦋 Changeset detected

Latest commit: 6cce591

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
Contributor

coderabbitai bot commented Mar 19, 2026

Walkthrough

Ports the existing JSX useSortedAttributes assist to HTML and introduces a shared sorting abstraction in biome_analyze: a public SortableAttribute trait and an AttributeGroup wrapper. Refactors the JSX implementation to use the shared abstractions and adds a new HTML assist rule (UseSortedAttributes) plus test fixtures across HTML, Astro, Svelte and Vue. Adds a dependency on biome_string_case and exposes SortableHtmlAttribute/SortableJsxAttribute implementations that provide attribute name tokens for comparisons.

Possibly related PRs

  • PR 9298 (biomejs/biome): touches the JSX use_sorted_attributes implementation and is directly related to the refactor that replaces local sorting helpers with the shared SortableAttribute abstraction.

Suggested labels

A-Project, A-Tooling

Suggested reviewers

  • dyc3
  • ematipico
  • Netail
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(assist): implement useSortedAttributes for HTML' accurately reflects the primary change: adding the useSortedAttributes assist rule to HTML.
Description check ✅ Passed The description clearly relates to the changeset, explaining the addition of useSortedAttributes for HTML and code refactoring to reduce duplication.
Linked Issues check ✅ Passed The PR fully implements the objective from issue #9334: porting the useSortedAttributes assist from JSX to HTML with feature parity and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are in-scope: new HTML rule implementation, shared trait abstraction, JSX refactoring to use shared code, and comprehensive test fixtures across multiple template languages.

✏️ 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.

Tip

You can customize the high-level summary generated by CodeRabbit.

Configure the reviews.high_level_summary_instructions setting to provide custom instructions for generating the high-level summary.

Copy link
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

codspeed-hq bot commented Mar 19, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 156 skipped benchmarks1


Comparing mujpao:feat/use-sorted-attributes (517e7d4) with next (ce65710)2

Open in CodSpeed

Footnotes

  1. 156 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.

  2. No successful run was found on next (4d251d4) during the generation of this report, so ce65710 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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
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
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

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
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
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
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
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
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
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.

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.

3 participants