Skip to content

feat(biome_html_analyze): port noRedundantRoles a11y rule to HTML#9564

Merged
dyc3 merged 10 commits intobiomejs:nextfrom
ctonneslan:port-no-redundant-roles-html-v2
Mar 22, 2026
Merged

feat(biome_html_analyze): port noRedundantRoles a11y rule to HTML#9564
dyc3 merged 10 commits intobiomejs:nextfrom
ctonneslan:port-no-redundant-roles-html-v2

Conversation

@ctonneslan
Copy link

Summary

Ports the noRedundantRoles accessibility lint rule from JSX to HTML, as part of #8155.

The rule flags explicit role attributes that match the element's implicit ARIA role (e.g. <button role="button">) and provides an unsafe fix action to remove the redundant attribute.

Implementation

  • Query type: Ast<AnyHtmlElement> (no special services needed)
  • Includes a get_implicit_role_for_element helper that maps HTML tag names to their implicit ARIA roles per the WAI-ARIA spec
  • Handles attribute-dependent implicit roles:
    • <input type="..."> — checkbox, radio, button, textbox, etc.
    • <th scope="col|row"> — columnheader vs rowheader
    • <a href="..."> — link vs generic
    • <img alt="..."> — img vs presentation
    • <section> with accessible name — region vs generic
    • <select> with size/multiple — combobox vs listbox

Test plan

  • invalid.html — 25 cases covering all common elements with redundant roles
  • valid.html — elements with different roles, no roles, and edge cases (a without href, img with empty alt)

Changeset

minor

Ref: #8155

@changeset-bot
Copy link

changeset-bot bot commented Mar 20, 2026

🦋 Changeset detected

Latest commit: 0d5579c

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-HTML Language: HTML and super languages labels Mar 20, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 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 new HTML accessibility lint rule NoRedundantRoles (noRedundantRoles, v2.4.0, recommended, Severity: Error, FixKind: Unsafe). The rule skips probable components (tag names starting with uppercase ASCII), trims and parses role attribute values, computes each element’s implicit ARIA role via get_implicit_role_for_element, and emits a diagnostic covering the role attribute when the explicit role equals the implicit one. An unsafe fix removes the role attribute node. New test fixtures added for HTML, Astro, Svelte and Vue covering valid and invalid cases.

Possibly related PRs

Suggested labels

A-Parser

Suggested reviewers

  • dyc3
  • ematipico
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: porting the noRedundantRoles accessibility rule to HTML analysis.
Description check ✅ Passed The description clearly explains the rule's purpose, implementation details, test coverage, and references the related issue.

✏️ 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
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/tests/specs/a11y/noRedundantRoles/invalid.html (1)

1-25: Consider adding test coverage for attribute-dependent implicit roles.

The PR objectives mention handling <input type="...">, <th scope="...">, <a href="...">, <img alt="...">, <section> with accessible name, and <select> with size/multiple. However, the invalid test cases don't exercise these attribute-dependent scenarios.

Consider adding cases such as:

  • <input type="checkbox" role="checkbox">
  • <input type="radio" role="radio">
  • <th scope="col" role="columnheader">
  • <select role="combobox">
  • <section aria-label="Section" role="region">

Also note: Line 6 <form role="form"> may be a false positive—per the WAI-ARIA spec, <form> only has an implicit role of form when it has an accessible name. Without one, it has no implicit role.

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

In `@crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalid.html`
around lines 1 - 25, Add attribute-dependent invalid cases and fix the form
false positive: add tests exercising implicit roles that depend on attributes
such as an <input type="checkbox" role="checkbox"> and <input type="radio"
role="radio">, a table header like <th scope="col" role="columnheader">, a
<select role="combobox"> (including size/multiple variations), and a landmark
<section aria-label="Section" role="region"> to ensure attribute-driven implicit
roles are covered; also update the existing <form role="form"> test to either
supply an accessible name (e.g., aria-label) so it legitimately has an implicit
form role or remove/mark it as valid since a bare <form> without an accessible
name does not have an implicit role per the WAI-ARIA spec.
crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs (1)

85-86: Minor: redundant .to_string() calls.

Both role_value and element_name_str are already String types (created via .to_lowercase()), so .to_string() is unnecessary here.

✨ Suggested fix
         if explicit_role == implicit_role {
             return Some(RuleState {
                 redundant_attribute: role_attribute,
-                role_value: role_value.to_string(),
-                element_name: element_name_str.to_string(),
+                role_value,
+                element_name: element_name_str,
             });
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs` around lines
85 - 86, The two fields role_value and element_name in the struct construction
are being assigned using redundant .to_string() calls even though role_value and
element_name_str are already String (from .to_lowercase()); remove the
unnecessary .to_string() calls and assign role_value and element_name_str
directly (e.g., role_value: role_value, element_name: element_name_str) in the
code inside no_redundant_roles.rs to eliminate the extra allocations.
🤖 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/lint/a11y/no_redundant_roles.rs`:
- Around line 85-86: The two fields role_value and element_name in the struct
construction are being assigned using redundant .to_string() calls even though
role_value and element_name_str are already String (from .to_lowercase());
remove the unnecessary .to_string() calls and assign role_value and
element_name_str directly (e.g., role_value: role_value, element_name:
element_name_str) in the code inside no_redundant_roles.rs to eliminate the
extra allocations.

In `@crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalid.html`:
- Around line 1-25: Add attribute-dependent invalid cases and fix the form false
positive: add tests exercising implicit roles that depend on attributes such as
an <input type="checkbox" role="checkbox"> and <input type="radio"
role="radio">, a table header like <th scope="col" role="columnheader">, a
<select role="combobox"> (including size/multiple variations), and a landmark
<section aria-label="Section" role="region"> to ensure attribute-driven implicit
roles are covered; also update the existing <form role="form"> test to either
supply an accessible name (e.g., aria-label) so it legitimately has an implicit
form role or remove/mark it as valid since a bare <form> without an accessible
name does not have an implicit role per the WAI-ARIA spec.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 410e8733-01e7-4e68-b88e-4dee5b3fb8fe

📥 Commits

Reviewing files that changed from the base of the PR and between d5913c9 and 00abd6a.

📒 Files selected for processing (3)
  • crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/valid.html

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

missing changeset

@ctonneslan
Copy link
Author

ctonneslan commented Mar 21, 2026

Thanks for the thorough review. Pushed fixes for everything:

  • Added changeset (minor)
  • Added RuleSource::HtmlEslint("no-redundant-role") as a second source
  • Skip uppercase elements (components) so this doesn't fire on <Button role="button"> in Vue/Svelte/Astro
  • Switched RuleState to store TextRange instead of cloning the HtmlAttribute
  • Removed the extra .to_string() allocations in RuleState
  • Added magic comments to test files
  • Added Vue, Svelte, and Astro test files (invalid + valid for each)
  • Prefixed unused state param in action

One thing I couldn't do cleanly: to_lowercase_cow() lives on TokenText via biome_string_case, but node.name() returns TokenText and the match arms in get_implicit_role_for_element need &str. I'm using .to_lowercase() for now since the element name is always short. Happy to change the approach if you have a preference.

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/lint/a11y/no_redundant_roles.rs`:
- Around line 181-214: The matching for HTML enumerated attributes in the "th"
and "input" branches is case-sensitive, so values like TYPE="CHECKBOX" or
SCoPe="COL" are missed; before matching use a normalized ASCII-case-insensitive
value (e.g., call to_ascii_lowercase or equivalent) for the results of
get_attribute_value for "type" and "scope" and then match on that normalized
string when producing AriaRole variants in the th and input match arms (retain
the existing list of cases and the list-check via
node.find_attribute_by_name("list")); also add mixed-case fixtures for at least
one input type branch and one th scope branch to prevent regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8815a7e8-8501-42f1-8e28-ab8b18b58aa8

📥 Commits

Reviewing files that changed from the base of the PR and between 00abd6a and 2a53285.

📒 Files selected for processing (10)
  • .changeset/port-no-redundant-roles-html.md
  • crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/valid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalid.html
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/invalid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/valid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/valid.html
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/valid.vue
✅ Files skipped from review due to trivial changes (7)
  • .changeset/port-no-redundant-roles-html.md
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/invalid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/valid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/invalid.vue
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/valid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/invalid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/valid.astro

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

You can dereference TokenText or call .text() on it to get a &str. You can also easily get a &str from a Cow string.

We really shouldn't allocate String unless its truly unavoidable, and I'm pretty confident its avoidable here.

@ctonneslan
Copy link
Author

Pushed the fixes. Main changes:

  • Dropped the .to_lowercase() calls entirely. HTML tag names come out of the parser already lowercased, and role values in the spec are lowercase too, so AriaRole::from_roles handles them fine as-is. Now I just grab &str from TokenText::text() and pass it through without allocating.
  • The only String allocation left is in RuleState for the diagnostic message, since the markup! macro needs owned strings. If there's a way to reference the original TokenText from the diagnostic I'm happy to change that too, but I think that would require storing the node itself.
  • Fixed the changeset to use past tense and link to the rule docs.
  • Added <Button role="button"> to the valid test cases for Vue/Svelte/Astro to explicitly cover the component skip path.

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/lint/a11y/no_redundant_roles.rs (1)

130-263: Consider reusing biome_aria::AriaRoles::get_implicit_role.

This helper duplicates much of the logic in crates/biome_aria/src/roles.rs. While the current implementation is correct and self-contained, sharing code would reduce maintenance burden and ensure consistency as the spec evolves.

Notable differences from biome_aria:

  • Missing menuitem handling (checkbox/radio variants)
  • Adds address, details, fieldset to Group (which biome_aria also has, so that's fine)

If reuse isn't straightforward due to the Element trait abstraction, this can be deferred.

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

In `@crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs` around lines
130 - 263, The get_implicit_role_for_element function duplicates ARIA role
logic; replace its long match with a single call to the shared implementation
(biome_aria::AriaRoles::get_implicit_role) to avoid drift: update
get_implicit_role_for_element to adapt the AnyHtmlElement/Element-trait access
to whatever API biome_aria expects (convert node attributes/name as needed) and
return the Option<AriaRole> from AriaRoles::get_implicit_role, ensuring you also
account for menuitem checkbox/radio variants if biome_aria exposes them (or map
them afterward) so behavior remains equivalent.
🤖 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/lint/a11y/no_redundant_roles.rs`:
- Around line 130-263: The get_implicit_role_for_element function duplicates
ARIA role logic; replace its long match with a single call to the shared
implementation (biome_aria::AriaRoles::get_implicit_role) to avoid drift: update
get_implicit_role_for_element to adapt the AnyHtmlElement/Element-trait access
to whatever API biome_aria expects (convert node attributes/name as needed) and
return the Option<AriaRole> from AriaRoles::get_implicit_role, ensuring you also
account for menuitem checkbox/radio variants if biome_aria exposes them (or map
them afterward) so behavior remains equivalent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7f2ae581-e287-48ea-abfb-5e630c847281

📥 Commits

Reviewing files that changed from the base of the PR and between 2a53285 and 4093dd0.

📒 Files selected for processing (5)
  • .changeset/port-no-redundant-roles-html.md
  • crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/valid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/valid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/valid.vue
✅ Files skipped from review due to trivial changes (4)
  • .changeset/port-no-redundant-roles-html.md
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/valid.astro
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/svelte/valid.svelte
  • crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/vue/valid.vue

@dyc3
Copy link
Contributor

dyc3 commented Mar 21, 2026

Token text is an owned type. You can just pass it in the state.

HTML tag names come out of the parser already lowercased,

I'm pretty sure it doesn't do that

@ctonneslan
Copy link
Author

ctonneslan commented Mar 21, 2026

  • Storing TokenText (element name) and Text (role value) directly in the state instead of allocating Strings. Both are owned types so they work fine in the state struct.
  • Switched to eq_ignore_ascii_case for all tag name comparisons. Had to change from match arms to an if-else chain since eq_ignore_ascii_case can't be used in match patterns, but it's the same logic.

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/lint/a11y/no_redundant_roles.rs`:
- Around line 325-341: The mapping currently returns AriaRole::Generic for
"header" and "footer" unconditionally; update the implicit-role logic in
no_redundant_roles (the code block that checks element_name and returns
AriaRole::Generic) to inspect ancestor elements: if element_name is "header" and
no ancestor is one of "article", "aside", "main", "nav", or "section" return
AriaRole::Banner; if element_name is "footer" and no such ancestor exists return
AriaRole::ContentInfo; otherwise fall back to AriaRole::Generic. Implement
ancestor traversal using the document/element context available in the same
function (or helper) so the header/footer cases are context-sensitive.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b816a423-02fc-4365-8249-9aedef52689d

📥 Commits

Reviewing files that changed from the base of the PR and between 4093dd0 and c970890.

📒 Files selected for processing (1)
  • crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs

Comment on lines +325 to +341
} else if element_name.eq_ignore_ascii_case("b")
|| element_name.eq_ignore_ascii_case("bdi")
|| element_name.eq_ignore_ascii_case("bdo")
|| element_name.eq_ignore_ascii_case("body")
|| element_name.eq_ignore_ascii_case("data")
|| element_name.eq_ignore_ascii_case("div")
|| element_name.eq_ignore_ascii_case("i")
|| element_name.eq_ignore_ascii_case("q")
|| element_name.eq_ignore_ascii_case("samp")
|| element_name.eq_ignore_ascii_case("small")
|| element_name.eq_ignore_ascii_case("span")
|| element_name.eq_ignore_ascii_case("u")
|| element_name.eq_ignore_ascii_case("pre")
|| element_name.eq_ignore_ascii_case("header")
|| element_name.eq_ignore_ascii_case("footer")
{
AriaRole::Generic
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's existing infrastructure for ancestor traversal in HTML rules
rg -n "parent|ancestor" --type rust crates/biome_html_analyze/src/lint/

Repository: biomejs/biome

Length of output: 4008


🏁 Script executed:

# First, let's examine the full no_redundant_roles.rs file to understand context handling
wc -l crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs

Repository: biomejs/biome

Length of output: 122


🏁 Script executed:

# Check if there are comments about header/footer limitations in the file
rg -n "header|footer" crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs -A 2 -B 2

Repository: biomejs/biome

Length of output: 658


🏁 Script executed:

# Look for the JSX equivalent rule to check if it documents this limitation
fd -t f "no_redundant_roles" crates/biome_js_analyze/

Repository: biomejs/biome

Length of output: 116


🌐 Web query:

WAI-ARIA header footer implicit roles context dependent banner contentinfo

💡 Result:

In WAI-ARIA, the HTML header and footer elements have implicit landmark roles that are context-dependent, specifically when used as direct descendants of the body element. The header element implies the banner role when it is in the context of a body element (i.e., a top-level page header containing site-oriented content like logos or global navigation). Similarly, the footer element implies the contentinfo role when it is in the context of a body element (i.e., a page footer with copyrights, privacy links, etc.). These implicit roles do not apply if header or footer are nested inside other elements like section, article, nav, main, or aside. In such cases, they do not convey landmark semantics and require explicit role attributes if needed. banner and contentinfo are landmark roles limited to one per page at the top level (unless nested in document/application iframes), and they help assistive technologies navigate page structure.

Citations:


🏁 Script executed:

# Read the no_redundant_roles.rs file to understand full context
head -100 crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs

Repository: biomejs/biome

Length of output: 2904


🏁 Script executed:

# Read around the header/footer section to see if there are any comments
sed -n '200,352p' crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs

Repository: biomejs/biome

Length of output: 6821


🏁 Script executed:

# Check the JSX rule for comparison
head -150 crates/biome_js_analyze/src/lint/a11y/no_redundant_roles.rs

Repository: biomejs/biome

Length of output: 3599


🏁 Script executed:

# Check if the JSX aria_roles service handles context for header/footer
rg -n "get_implicit_role|header|footer" crates/biome_js_analyze/src/services/aria/ -A 3 -B 1

Repository: biomejs/biome

Length of output: 139


🏁 Script executed:

# Find where the aria service is located
find crates -type d -name "aria" 2>/dev/null

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Search for get_implicit_role implementation
rg -n "get_implicit_role" --type rust crates/

Repository: biomejs/biome

Length of output: 1033


🏁 Script executed:

# Check the biome_aria implementation of get_implicit_role
cat -n crates/biome_aria/src/roles.rs | head -50

Repository: biomejs/biome

Length of output: 2440


🏁 Script executed:

# Read more of the roles.rs to understand get_implicit_role implementation
sed -n '1,100p' crates/biome_aria/src/roles.rs

Repository: biomejs/biome

Length of output: 4312


🏁 Script executed:

# Search for header/footer in the aria metadata
rg -n "header|footer" crates/biome_aria/src/roles.rs -A 2 -B 2

Repository: biomejs/biome

Length of output: 1850


Context-dependent roles for <header> and <footer> are not handled.

Per WAI-ARIA, <header> has implicit role banner (and <footer> has contentinfo) when not nested in <article>, <aside>, <main>, <nav>, or <section>. The current implementation unconditionally returns Generic for both. This is a documented limitation in the aria metadata layer: ancestor traversal would be needed to determine the correct implicit role.

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

In `@crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs` around lines
325 - 341, The mapping currently returns AriaRole::Generic for "header" and
"footer" unconditionally; update the implicit-role logic in no_redundant_roles
(the code block that checks element_name and returns AriaRole::Generic) to
inspect ancestor elements: if element_name is "header" and no ancestor is one of
"article", "aside", "main", "nav", or "section" return AriaRole::Banner; if
element_name is "footer" and no such ancestor exists return
AriaRole::ContentInfo; otherwise fall back to AriaRole::Generic. Implement
ancestor traversal using the document/element context available in the same
function (or helper) so the header/footer cases are context-sensitive.

@dyc3
Copy link
Contributor

dyc3 commented Mar 21, 2026

there's still no .snap files for the tests... you need to generate them with cargo test and accept them with cargo insta

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 21, 2026

Merging this PR will degrade performance by 24.27%

❌ 1 (👁 1) regressed benchmark
✅ 63 untouched benchmarks
⏩ 152 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
👁 html_analyzer[index_1033418810622582172.html] 442.6 µs 584.4 µs -24.27%

Comparing ctonneslan:port-no-redundant-roles-html-v2 (0d5579c) with next (14920a8)

Open in CodSpeed

Footnotes

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

@github-actions github-actions bot added the A-CLI Area: CLI label Mar 21, 2026
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: 2

♻️ Duplicate comments (1)
crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs (1)

251-274: ⚠️ Potential issue | 🟡 Minor

scope and type still need ASCII-case normalisation.

Lines 251-274 compare raw attribute text, so scope="COL" and type="CHECKBOX" still dodge the rule. Please normalise these enumerated values before matching, and add one mixed-case fixture per branch so it stays fixed this time.

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

In `@crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs` around lines
251 - 274, The attribute comparisons for scope and type use raw attribute text
so mixed-case values slip through; update get_attribute_value calls in the
branch that determines AriaRole to normalize ASCII case (e.g., map the
Option<String> to to_ascii_lowercase) before doing match on
scope_value.as_deref() and type_value.as_deref(), keeping the existing match
arms (Some("col"), Some("checkbox"), etc.), and also add one test fixture per
branch using a mixed-case attribute (e.g., scope="CoL", type="ChEcKbOx") to
ensure the rule catches case variations; refer to get_attribute_value, scope,
type, AriaRole, and node.find_attribute_by_name to locate the code to change.
🤖 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/lint/a11y/no_redundant_roles.rs`:
- Around line 283-290: The element-to-role mapping for anchors misses the "link"
element, causing <link href role="link"> to be treated as Generic; update the
conditional in the mapper that checks element_name (the branch that currently
tests "a" and "area") to also accept "link" (e.g., include
element_name.eq_ignore_ascii_case("link")) so that when
node.find_attribute_by_name("href").is_some() the function returns
AriaRole::Link; alternatively refactor this mapping to call the shared mapper in
crates::biome_aria::roles.rs to avoid future drift.
- Around line 77-85: The PascalCase early-return should only run for framework
template files, not plain HTML; update the check around
element_name.text().as_bytes().first().is_some_and(u8::is_ascii_uppercase) to
first verify the file is a framework template via ctx.file_path() (e.g., ends
with .vue, .svelte, .astro) and only then skip by returning None. Locate the
PascalCase check in no_redundant_roles (the block using element_name and the
early return) and wrap it with a guard that inspects ctx.file_path() for those
template extensions before applying the uppercase-first-char heuristic.

---

Duplicate comments:
In `@crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs`:
- Around line 251-274: The attribute comparisons for scope and type use raw
attribute text so mixed-case values slip through; update get_attribute_value
calls in the branch that determines AriaRole to normalize ASCII case (e.g., map
the Option<String> to to_ascii_lowercase) before doing match on
scope_value.as_deref() and type_value.as_deref(), keeping the existing match
arms (Some("col"), Some("checkbox"), etc.), and also add one test fixture per
branch using a mixed-case attribute (e.g., scope="CoL", type="ChEcKbOx") to
ensure the rule catches case variations; refer to get_attribute_value, scope,
type, AriaRole, and node.find_attribute_by_name to locate the code to change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 63ca808c-87f1-4b34-8c60-6f6a61b375db

📥 Commits

Reviewing files that changed from the base of the PR and between c970890 and 6e2c9ca.

⛔ Files ignored due to path filters (1)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
📒 Files selected for processing (1)
  • crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs

@ctonneslan
Copy link
Author

ctonneslan commented Mar 21, 2026

  1. Removed the PascalCase early return entirely. It was wrong for plain HTML since <BUTTON role="button"> would skip the rule. Components like <MyComponent> naturally return None from the implicit role lookup since they don't match any HTML tag, so they're already handled without the skip.

  2. Added link to the a/area branch to match the shared mapper in biome_aria/src/roles.rs.

@ctonneslan
Copy link
Author

Addressed the remaining feedback:

  1. Snapshots added. Generated with cargo insta test and accepted for all 8 test files (HTML, Vue, Svelte, Astro, both valid and invalid).

  2. Case-insensitive attribute values. Added get_attribute_value_lowercase for input type and th scope since HTML enumerated attributes are ASCII case-insensitive per spec. So type="CHECKBOX" and scope="COL" now match correctly.

  3. File-type-aware component skip. Used ctx.source_type::<HtmlFileSource>().is_html() to only skip PascalCase elements in non-HTML files (Vue, Svelte, Astro). In plain HTML, <BUTTON role="button"> still gets matched since HTML tags are case-insensitive. Found this pattern in no_sync_scripts.rs.

All 8 tests pass.

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

note the CI failures, and please address the codspeed perf regression

@ctonneslan
Copy link
Author

Fixed both CI issues:

  1. Clippy disallowed-methods: Replaced to_ascii_lowercase() with to_ascii_lowercase_cow() from biome_string_case. This avoids allocation when the value is already lowercase (which it usually is).

  2. CodSpeed perf regression: The if-else chain with eq_ignore_ascii_case on every branch was slow. Switched back to a match table by lowercasing the element name once upfront with to_ascii_lowercase_cow() in run(), then passing the Cow<str> into get_implicit_role_for_element which uses the original match dispatch. Same approach for input type and th scope attribute values.

Net result: 75 lines added, 156 removed. Match table is back, allocations are minimized, and the disallowed method is gone.

@Netail Netail mentioned this pull request Mar 21, 2026
32 tasks
Copy link
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.

I believe the current benchmark is low signal because its so small. Looks good.

@dyc3 dyc3 self-assigned this Mar 21, 2026
@dyc3
Copy link
Contributor

dyc3 commented Mar 21, 2026

@ctonneslan Its pretty obvious you've been using ai (which you should have disclosed, per our contributing guidelines and pr template, please do so in the future), but just curious, what model were you using?

@dyc3 dyc3 force-pushed the port-no-redundant-roles-html-v2 branch from 2ecdd0b to 0bcd5fe Compare March 21, 2026 22:21
@dyc3
Copy link
Contributor

dyc3 commented Mar 21, 2026

Opus 4.6?

ctonneslan and others added 10 commits March 22, 2026 09:35
Port the noRedundantRoles lint rule from JSX to HTML, as part of
the umbrella issue biomejs#8155.

The rule flags explicit role attributes that match the element's
implicit ARIA role (e.g. <button role="button">) and provides an
unsafe fix to remove the redundant attribute.

Includes implicit role resolution for all standard HTML elements,
including attribute-dependent cases (input type, th scope, a href,
img alt, section with accessible name, select size/multiple).

Ref: biomejs#8155
- Add minor changeset
- Add HtmlEslint('no-redundant-role') as second rule source
- Skip component elements (uppercase) in Vue/Svelte/Astro files
- Store TextRange instead of HtmlAttribute in RuleState
- Remove unnecessary String allocations in RuleState
- Add magic comments to test files
- Add Vue, Svelte, and Astro test files
- Prefix unused state param in action
- Work with &str from TokenText directly instead of calling
  .to_lowercase() (HTML tag names are already lowercase from parser)
- Only allocate String in RuleState for the diagnostic message
- Use past tense and add docs link in changeset
- Add <Button role="button"> as valid test case in Vue/Svelte/Astro
  to verify component elements are correctly skipped
- Store TokenText (element name) and Text (role value) directly in
  RuleState instead of allocating Strings. Both are owned types.
- Use eq_ignore_ascii_case for tag name matching since the HTML parser
  preserves original casing from source (doesn't lowercase).
- Switched from match arms to if-else chain since eq_ignore_ascii_case
  can't be used in match patterns.
- Removed the PascalCase early return. Components like <MyComponent>
  naturally return None from get_implicit_role_for_element since they
  don't match any tag name. The old skip would have incorrectly
  bypassed <BUTTON role="button"> in plain HTML files.
- Added 'link' to the a/area implicit role branch to match the
  shared mapper in biome_aria/src/roles.rs.
… skip

- Generate and accept insta snapshots for all test files (HTML, Vue,
  Svelte, Astro)
- Normalize input type and th scope attribute values with
  to_ascii_lowercase per HTML spec (enumerated attributes are ASCII
  case-insensitive)
- Gate the PascalCase component skip to non-HTML files using
  ctx.source_type::<HtmlFileSource>().is_html() so <BUTTON> in plain
  HTML still gets matched while <Button> in Vue/Svelte/Astro is
  correctly skipped
- Replace disallowed to_ascii_lowercase() with
  to_ascii_lowercase_cow() from biome_string_case
- Lowercase the element name once in run() with
  to_ascii_lowercase_cow, then pass the Cow<str> to
  get_implicit_role_for_element which uses a match table
- This restores the match-based dispatch (faster than the if-else
  chain) while still handling case-insensitive HTML tag names
- Also lowercase input type and th scope values with the same
  cow-based approach
@dyc3 dyc3 force-pushed the port-no-redundant-roles-html-v2 branch from 2de8e67 to 0d5579c Compare March 22, 2026 13:35
@dyc3 dyc3 merged commit 0e0dbdf into biomejs:next Mar 22, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants