-
-
Notifications
You must be signed in to change notification settings - Fork 930
feat(html/a11y): add noRedundantRoles rule for HTML #9276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
71e14cd
feat(html/a11y): add noRedundantRoles rule for HTML
IxxyDev 393fc5c
feat(html/a11y): address PR review feedback
IxxyDev d338d83
fix: address PR review feedback
IxxyDev 58f38d1
chore: remove redundant comments
IxxyDev 567b18f
fix: skip auto-fix for multi-token role values
IxxyDev d52543f
test: add multi-token role test cases for noRedundantRoles
IxxyDev 323d600
[autofix.ci] apply automated fixes
autofix-ci[bot] 6ffccf0
Merge remote-tracking branch 'upstream/next' into feat/html-no-redund…
IxxyDev 89399c0
chore: resolve merge conflict in rule.rs after pulling remote changes
IxxyDev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| "@biomejs/biome": minor | ||
| --- | ||
|
|
||
| Added the HTML lint rule [`noRedundantRoles`](https://biomejs.dev/linter/rules/no-redundant-roles/). This rule enforces that explicit `role` attributes are not the same as the implicit/default role of an HTML element. It supports HTML, Vue, Svelte, and Astro files. | ||
|
|
||
| ```html | ||
| <!-- Invalid: role="button" is redundant on <button> --> | ||
| <button role="button"></button> | ||
| ``` | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
8 changes: 8 additions & 0 deletions
8
crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| use biome_analyze::{ | ||
| Ast, FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, | ||
| }; | ||
| use biome_aria::AriaRoles; | ||
| use biome_aria_metadata::AriaRole; | ||
| use biome_console::markup; | ||
| use biome_diagnostics::Severity; | ||
| use biome_html_syntax::{AnyHtmlElement, HtmlAttribute, HtmlFileSource}; | ||
| use biome_rowan::{AstNode, BatchMutationExt, Text}; | ||
| use biome_rule_options::no_redundant_roles::NoRedundantRolesOptions; | ||
|
|
||
| use crate::HtmlRuleAction; | ||
|
|
||
| declare_lint_rule! { | ||
| /// Enforce explicit `role` property is not the same as implicit/default role property on an element. | ||
| /// | ||
| /// :::note | ||
| /// In `.html` files, all elements are treated as native HTML elements. | ||
| /// | ||
| /// In component-based frameworks (Vue, Svelte, Astro), only native HTML element names are checked. | ||
| /// PascalCase names like `<Button>` and kebab-case names like `<my-button>` are assumed to be | ||
| /// custom components and are ignored. | ||
| /// ::: | ||
| /// | ||
| /// ## Examples | ||
| /// | ||
| /// ### Invalid | ||
| /// | ||
| /// ```html,expect_diagnostic | ||
| /// <article role="article"></article> | ||
| /// ``` | ||
| /// | ||
| /// ```html,expect_diagnostic | ||
| /// <button role="button"></button> | ||
| /// ``` | ||
| /// | ||
| /// ```html,expect_diagnostic | ||
| /// <h1 role="heading" aria-level="1">title</h1> | ||
| /// ``` | ||
| /// | ||
| /// ### Valid | ||
| /// | ||
| /// ```html | ||
| /// <article role="presentation"></article> | ||
| /// ``` | ||
| /// | ||
| /// ```html | ||
| /// <span></span> | ||
| /// ``` | ||
| /// | ||
| pub NoRedundantRoles { | ||
| version: "next", | ||
| name: "noRedundantRoles", | ||
| language: "html", | ||
| sources: &[RuleSource::EslintJsxA11y("no-redundant-roles").same(), RuleSource::HtmlEslint("no-redundant-role").same()], | ||
| recommended: true, | ||
| severity: Severity::Error, | ||
| fix_kind: FixKind::Unsafe, | ||
| } | ||
| } | ||
|
|
||
| impl Rule for NoRedundantRoles { | ||
| type Query = Ast<AnyHtmlElement>; | ||
| type State = RuleState; | ||
| type Signals = Option<Self::State>; | ||
| type Options = NoRedundantRolesOptions; | ||
|
|
||
| fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
| let node = ctx.query(); | ||
|
|
||
| let source_type = ctx.source_type::<HtmlFileSource>(); | ||
| if !source_type.is_html() { | ||
| let element_name = node.name()?; | ||
| let name_text = element_name.text(); | ||
| if name_text.chars().next().is_some_and(|c| c.is_uppercase()) | ||
| || name_text.contains('-') | ||
| { | ||
| return None; | ||
| } | ||
| } | ||
|
|
||
| let role_attribute = node.find_attribute_by_name("role")?; | ||
| let role_attribute_value = role_attribute.initializer()?.value().ok()?.string_value()?; | ||
| let trimmed = role_attribute_value.trim(); | ||
| let explicit_role = AriaRole::from_roles(trimmed)?; | ||
|
|
||
| if AriaRoles.get_implicit_role(node)? == explicit_role { | ||
| let has_multiple_roles = trimmed.split_ascii_whitespace().nth(1).is_some(); | ||
| return Some(RuleState { | ||
| redundant_attribute: role_attribute, | ||
| role_attribute_value, | ||
| has_multiple_roles, | ||
| }); | ||
| } | ||
| None | ||
| } | ||
|
|
||
| fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { | ||
| let element_name = ctx.query().name()?; | ||
| let element_name = element_name.text(); | ||
| let role_attribute = state.role_attribute_value.to_string(); | ||
| Some(RuleDiagnostic::new( | ||
| rule_category!(), | ||
| state.redundant_attribute.range(), | ||
| markup! { | ||
| "Using the role attribute '"{role_attribute}"' on the '"{element_name}"' element is redundant, because it is implied by its semantics." | ||
| }, | ||
| )) | ||
| } | ||
|
|
||
| fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<HtmlRuleAction> { | ||
| if state.has_multiple_roles { | ||
| return None; | ||
| } | ||
| let mut mutation = ctx.root().begin(); | ||
| mutation.remove_node(state.redundant_attribute.clone()); | ||
| Some(HtmlRuleAction::new( | ||
| ctx.metadata().action_category(ctx.category(), ctx.group()), | ||
| ctx.metadata().applicability(), | ||
| markup! { "Remove the "<Emphasis>"role"</Emphasis>" attribute." }.to_owned(), | ||
| mutation, | ||
| )) | ||
| } | ||
| } | ||
|
|
||
| pub struct RuleState { | ||
| redundant_attribute: HtmlAttribute, | ||
| role_attribute_value: Text, | ||
| has_multiple_roles: bool, | ||
| } |
9 changes: 9 additions & 0 deletions
9
crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/invalid.astro
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| // Astro frontmatter | ||
| --- | ||
|
|
||
| <!-- Native elements with redundant roles should still trigger --> | ||
| <article role="article"></article> | ||
| <button role="button"></button> | ||
| <h1 role="heading" aria-level="1">title</h1> | ||
| <nav role="navigation"></nav> |
92 changes: 92 additions & 0 deletions
92
crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/invalid.astro.snap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| --- | ||
| source: crates/biome_html_analyze/tests/spec_tests.rs | ||
| expression: invalid.astro | ||
| --- | ||
| # Input | ||
| ```html | ||
| --- | ||
| // Astro frontmatter | ||
| --- | ||
|
|
||
| <!-- Native elements with redundant roles should still trigger --> | ||
| <article role="article"></article> | ||
| <button role="button"></button> | ||
| <h1 role="heading" aria-level="1">title</h1> | ||
| <nav role="navigation"></nav> | ||
|
|
||
| ``` | ||
|
|
||
| # Diagnostics | ||
| ``` | ||
| invalid.astro:6:10 lint/a11y/noRedundantRoles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Using the role attribute 'article' on the 'article' element is redundant, because it is implied by its semantics. | ||
|
|
||
| 5 │ <!-- Native elements with redundant roles should still trigger --> | ||
| > 6 │ <article role="article"></article> | ||
| │ ^^^^^^^^^^^^^^ | ||
| 7 │ <button role="button"></button> | ||
| 8 │ <h1 role="heading" aria-level="1">title</h1> | ||
|
|
||
| i Unsafe fix: Remove the role attribute. | ||
|
|
||
| 6 │ <article·role="article"></article> | ||
| │ -------------- | ||
|
|
||
| ``` | ||
|
|
||
| ``` | ||
| invalid.astro:7:9 lint/a11y/noRedundantRoles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Using the role attribute 'button' on the 'button' element is redundant, because it is implied by its semantics. | ||
|
|
||
| 5 │ <!-- Native elements with redundant roles should still trigger --> | ||
| 6 │ <article role="article"></article> | ||
| > 7 │ <button role="button"></button> | ||
| │ ^^^^^^^^^^^^^ | ||
| 8 │ <h1 role="heading" aria-level="1">title</h1> | ||
| 9 │ <nav role="navigation"></nav> | ||
|
|
||
| i Unsafe fix: Remove the role attribute. | ||
|
|
||
| 7 │ <button·role="button"></button> | ||
| │ ------------- | ||
|
|
||
| ``` | ||
|
|
||
| ``` | ||
| invalid.astro:8:5 lint/a11y/noRedundantRoles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Using the role attribute 'heading' on the 'h1' element is redundant, because it is implied by its semantics. | ||
|
|
||
| 6 │ <article role="article"></article> | ||
| 7 │ <button role="button"></button> | ||
| > 8 │ <h1 role="heading" aria-level="1">title</h1> | ||
| │ ^^^^^^^^^^^^^^ | ||
| 9 │ <nav role="navigation"></nav> | ||
| 10 │ | ||
|
|
||
| i Unsafe fix: Remove the role attribute. | ||
|
|
||
| 8 │ <h1·role="heading"·aria-level="1">title</h1> | ||
| │ --------------- | ||
|
|
||
| ``` | ||
|
|
||
| ``` | ||
| invalid.astro:9:6 lint/a11y/noRedundantRoles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| × Using the role attribute 'navigation' on the 'nav' element is redundant, because it is implied by its semantics. | ||
|
|
||
| 7 │ <button role="button"></button> | ||
| 8 │ <h1 role="heading" aria-level="1">title</h1> | ||
| > 9 │ <nav role="navigation"></nav> | ||
| │ ^^^^^^^^^^^^^^^^^ | ||
| 10 │ | ||
|
|
||
| i Unsafe fix: Remove the role attribute. | ||
|
|
||
| 9 │ <nav·role="navigation"></nav> | ||
| │ ----------------- | ||
|
|
||
| ``` |
21 changes: 21 additions & 0 deletions
21
crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/valid.astro
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| --- | ||
| // Astro frontmatter | ||
| --- | ||
|
|
||
| <!-- Custom components should not trigger the rule --> | ||
| <Button role="button"></Button> | ||
| <Nav role="navigation"></Nav> | ||
| <Article role="article"></Article> | ||
| <Dialog role="dialog"></Dialog> | ||
| <Form role="form"></Form> | ||
| <Table role="table"></Table> | ||
|
|
||
| <!-- Kebab-case custom elements should not trigger the rule --> | ||
| <my-button role="button"></my-button> | ||
| <my-nav role="navigation"></my-nav> | ||
| <my-article role="article"></my-article> | ||
|
|
||
| <!-- Native elements with non-redundant roles --> | ||
| <article role="presentation"></article> | ||
| <span></span> | ||
| <div></div> |
29 changes: 29 additions & 0 deletions
29
crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/astro/valid.astro.snap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| --- | ||
| source: crates/biome_html_analyze/tests/spec_tests.rs | ||
| expression: valid.astro | ||
| --- | ||
| # Input | ||
| ```html | ||
| --- | ||
| // Astro frontmatter | ||
| --- | ||
|
|
||
| <!-- Custom components should not trigger the rule --> | ||
| <Button role="button"></Button> | ||
| <Nav role="navigation"></Nav> | ||
| <Article role="article"></Article> | ||
| <Dialog role="dialog"></Dialog> | ||
| <Form role="form"></Form> | ||
| <Table role="table"></Table> | ||
|
|
||
| <!-- Kebab-case custom elements should not trigger the rule --> | ||
| <my-button role="button"></my-button> | ||
| <my-nav role="navigation"></my-nav> | ||
| <my-article role="article"></my-article> | ||
|
|
||
| <!-- Native elements with non-redundant roles --> | ||
| <article role="presentation"></article> | ||
| <span></span> | ||
| <div></div> | ||
|
|
||
| ``` |
27 changes: 27 additions & 0 deletions
27
crates/biome_html_analyze/tests/specs/a11y/noRedundantRoles/invalid.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <article role="article"></article> | ||
| <button role="button"></button> | ||
| <h1 role="heading" aria-level="1">title</h1> | ||
| <dialog role="dialog"></dialog> | ||
| <input type="checkbox" role="checkbox" /> | ||
| <figure role="figure"></figure> | ||
| <form role="form"></form> | ||
| <fieldset role="group"></fieldset> | ||
| <img src="foo" alt="bar" role="img" /> | ||
| <img alt="" role="presentation" /> | ||
| <a href="#" role="link"></a> | ||
| <ol role="list"></ol> | ||
| <ul role="list"></ul> | ||
| <select name="name" role="combobox"></select> | ||
| <select name="name" multiple size="4" role="listbox"></select> | ||
| <li role="listitem"></li> | ||
| <nav role="navigation"></nav> | ||
| <tr role="row"></tr> | ||
| <tbody role="rowgroup"></tbody> | ||
| <tfoot role="rowgroup"></tfoot> | ||
| <thead role="rowgroup"></thead> | ||
| <input type="search" role="searchbox" /> | ||
| <table role="table"></table> | ||
| <textarea role="textbox"></textarea> | ||
| <input type="text" role="textbox" /> | ||
| <button role="button presentation"></button> | ||
| <nav role="navigation link"></nav> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.