diff --git a/.changeset/port-no-redundant-roles-html.md b/.changeset/port-no-redundant-roles-html.md new file mode 100644 index 000000000000..66f7717a8745 --- /dev/null +++ b/.changeset/port-no-redundant-roles-html.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": minor +--- + +Ported [`noRedundantRoles`](https://biomejs.dev/linter/rules/no-redundant-roles/) a11y lint rule to HTML, Vue, Svelte, and Astro files. diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index 19098c2ea68f..60e8e3eb8067 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -249,6 +249,14 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "@html-eslint/no-redundant-role" => { + let group = rules.a11y.get_or_insert_with(Default::default); + let rule = group + .unwrap_group_as_mut() + .no_redundant_roles + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "@html-eslint/require-button-type" => { let group = rules.a11y.get_or_insert_with(Default::default); let rule = group diff --git a/crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs b/crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs new file mode 100644 index 000000000000..3952a03d8570 --- /dev/null +++ b/crates/biome_html_analyze/src/lint/a11y/no_redundant_roles.rs @@ -0,0 +1,360 @@ +use biome_analyze::{ + Ast, FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_aria_metadata::AriaRole; +use biome_console::markup; +use biome_diagnostics::Severity; +use biome_html_syntax::{AnyHtmlElement, HtmlFileSource}; +use biome_rowan::{AstNode, AstNodeList, BatchMutationExt, Text, TextRange, TokenText}; +use biome_rule_options::no_redundant_roles::NoRedundantRolesOptions; +use biome_string_case::StrLikeExtension; + +use crate::HtmlRuleAction; + +declare_lint_rule! { + /// Enforce explicit `role` property is not the same as implicit/default role property on an element. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```html,expect_diagnostic + ///
+ /// ``` + /// + /// ```html,expect_diagnostic + /// + /// ``` + /// + /// ```html,expect_diagnostic + ///

title

+ /// ``` + /// + /// ### Valid + /// + /// ```html + ///
+ /// ``` + /// + /// ```html + ///
+ /// ``` + /// + /// ```html + /// + /// ``` + /// + 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, + } +} + +pub struct RuleState { + attribute_range: TextRange, + role_value: Text, + element_name: TokenText, +} + +impl Rule for NoRedundantRoles { + type Query = Ast; + type State = RuleState; + type Signals = Option; + type Options = NoRedundantRolesOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + + // Fast path: elements with no attributes can't have a role attribute. + if node.attributes().is_none_or(|a| a.is_empty()) { + return None; + } + + let element_name = node.name()?; + + // In non-HTML files (Vue, Svelte, Astro), PascalCase elements like + //