diff --git a/crates/biome_aria/src/roles.rs b/crates/biome_aria/src/roles.rs index 4e0a7ec57c61..526b07b1eb68 100644 --- a/crates/biome_aria/src/roles.rs +++ b/crates/biome_aria/src/roles.rs @@ -1019,7 +1019,10 @@ impl<'a> AriaRoles { "code" => &CodeRole as &dyn AriaRoleDefinition, "datalist" => &ListBoxRole as &dyn AriaRoleDefinition, "del" => &DeletionRole as &dyn AriaRoleDefinition, + "dd" => &DefinitionRole as &dyn AriaRoleDefinition, + "dt" => &TermRole as &dyn AriaRoleDefinition, "dfn" => &TermRole as &dyn AriaRoleDefinition, + "mark" => &MarkRole as &dyn AriaRoleDefinition, "dialog" => &DialogRole as &dyn AriaRoleDefinition, "em" => &EmphasisRole as &dyn AriaRoleDefinition, "figure" => &FigureRole as &dyn AriaRoleDefinition, @@ -1035,6 +1038,7 @@ impl<'a> AriaRoles { "nav" => &NavigationRole as &dyn AriaRoleDefinition, "ul" | "ol" => &ListRole as &dyn AriaRoleDefinition, "li" => &ListItemRole as &dyn AriaRoleDefinition, + "option" => &OptionRole as &dyn AriaRoleDefinition, "optgroup" => &GroupRole as &dyn AriaRoleDefinition, "output" => &StatusRole as &dyn AriaRoleDefinition, "p" => &ParagraphRole as &dyn AriaRoleDefinition, @@ -1047,6 +1051,16 @@ impl<'a> AriaRoles { "table" => &TableRole as &dyn AriaRoleDefinition, "textarea" => &TextboxRole as &dyn AriaRoleDefinition, "tr" => &RowRole as &dyn AriaRoleDefinition, + // cell if a descendant of a element, + // but this crate does not support checking a descendant of an element. + // + // ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td + "td" => &CellRole as &dyn AriaRoleDefinition, + //
element is able to be a rowheader, columnheader, + // but this crate does not support checking a descendant of an element. + // + // ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th + "th" => &RowHeaderRole as &dyn AriaRoleDefinition, "time" => &TimeRole as &dyn AriaRoleDefinition, "address" | "details" | "fieldset" => &GroupRole as &dyn AriaRoleDefinition, "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => &HeadingRole as &dyn AriaRoleDefinition, @@ -1115,7 +1129,7 @@ impl<'a> AriaRoles { } } "b" | "bdi" | "bdo" | "body" | "data" | "div" | "hgroup" | "i" | "q" | "samp" - | "small" | "span" | "u" => &GenericRole as &dyn AriaRoleDefinition, + | "small" | "span" | "u" | "pre" => &GenericRole as &dyn AriaRoleDefinition, "header" | "footer" => { // This crate does not support checking a descendant of an element. // header (maybe BannerRole): https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/banner.html @@ -1296,6 +1310,44 @@ impl<'a> AriaRoles { role_candidate.concepts_by_role() } + + pub fn is_not_static_element( + &self, + element_name: &str, + attributes: &FxHashMap>, + ) -> bool { + if match element_name { + // embedded content + // ref: https://html.spec.whatwg.org/multipage/semantics.html#embedded-content + "canvas" | "embed" | "iframe" | "video" | "audio" => true, + // metadata content + // ref: https://html.spec.whatwg.org/multipage/semantics.html#document-metadata + "meta" | "link" | "base" | "title" | "basefont" | "head" => false, + // scripting content + // ref: https://html.spec.whatwg.org/multipage/semantics.html#scripting-content + "script" | "noscript" | "template" | "style" => false, + // No corresponding role + "dl" | "label" | "legend" | "ruby" | "pre" | "figcaption" | "br" => true, + _ => false, + } { + return true; + } + + // if the element has a interactive role, it is considered interactive. + let role_name = attributes + .get("role") + .and_then(|role| role.first()) + .map_or_else( + || self.get_implicit_role(element_name, attributes), + |r| self.get_role(r), + ); + + match role_name.map(|role| role.type_name()) { + Some("biome_aria::roles::PresentationRole" | "biome_aria::roles::GenericRole") => false, + Some(_) => true, + None => false, + } + } } type ElementsAndAttributes<'a> = Option>; 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 539a36f820d1..1c914b1d625e 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 @@ -649,6 +649,16 @@ pub(crate) fn migrate_eslint_any_rule( let rule = group.no_redundant_roles.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "jsx-a11y/no-static-element-interactions" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .no_static_element_interactions + .get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "jsx-a11y/prefer-tag-over-role" => { if !options.include_nursery { return false; diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index f35e445b9cfb..a2859dc56572 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2878,6 +2878,9 @@ pub struct Nursery { #[doc = "Disallow shorthand properties that override related longhand properties."] #[serde(skip_serializing_if = "Option::is_none")] pub no_shorthand_property_overrides: Option>, + #[doc = "Enforce that static, visible elements (such as \\
) that have click handlers use the valid role attribute."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_static_element_interactions: Option>, #[doc = "Enforce the use of String.slice() over String.substr() and String.substring()."] #[serde(skip_serializing_if = "Option::is_none")] pub no_substr: Option>, @@ -3023,6 +3026,7 @@ impl Nursery { "noReactSpecificProps", "noRestrictedImports", "noShorthandPropertyOverrides", + "noStaticElementInteractions", "noSubstr", "noUndeclaredDependencies", "noUnknownFunction", @@ -3098,17 +3102,17 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3166,6 +3170,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3287,176 +3292,181 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_substr.as_ref() { + if let Some(rule) = self.no_static_element_interactions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_substr.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.no_unknown_function.as_ref() { + if let Some(rule) = self.no_undeclared_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.no_unknown_media_feature_name.as_ref() { + if let Some(rule) = self.no_unknown_function.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.no_unknown_property.as_ref() { + if let Some(rule) = self.no_unknown_media_feature_name.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.no_unknown_pseudo_class_selector.as_ref() { + if let Some(rule) = self.no_unknown_property.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_class_selector.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.no_unknown_unit.as_ref() { + if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { + if let Some(rule) = self.no_unknown_unit.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.no_unused_function_parameters.as_ref() { + if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.no_useless_string_concat.as_ref() { + if let Some(rule) = self.no_unused_function_parameters.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { + if let Some(rule) = self.no_useless_string_concat.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.no_yoda_expression.as_ref() { + if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { + if let Some(rule) = self.no_yoda_expression.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { + if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.use_consistent_curly_braces.as_ref() { + if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.use_consistent_grid_areas.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.use_date_now.as_ref() { + if let Some(rule) = self.use_consistent_grid_areas.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.use_default_switch_clause.as_ref() { + if let Some(rule) = self.use_date_now.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_default_switch_clause.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.use_error_message.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } - if let Some(rule) = self.use_explicit_length_check.as_ref() { + if let Some(rule) = self.use_error_message.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - if let Some(rule) = self.use_focusable_interactive.as_ref() { + if let Some(rule) = self.use_explicit_length_check.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } - if let Some(rule) = self.use_generic_font_names.as_ref() { + if let Some(rule) = self.use_focusable_interactive.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } } - if let Some(rule) = self.use_import_extensions.as_ref() { + if let Some(rule) = self.use_generic_font_names.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_import_extensions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46])); } } - if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } } - if let Some(rule) = self.use_semantic_elements.as_ref() { + if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_semantic_elements.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + if let Some(rule) = self.use_sorted_classes.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); } } - if let Some(rule) = self.use_throw_new_error.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } } - if let Some(rule) = self.use_throw_only_error.as_ref() { + if let Some(rule) = self.use_throw_new_error.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52])); } } - if let Some(rule) = self.use_top_level_regex.as_ref() { + if let Some(rule) = self.use_throw_only_error.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_top_level_regex.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3566,176 +3576,181 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_substr.as_ref() { + if let Some(rule) = self.no_static_element_interactions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_substr.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.no_unknown_function.as_ref() { + if let Some(rule) = self.no_undeclared_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.no_unknown_media_feature_name.as_ref() { + if let Some(rule) = self.no_unknown_function.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.no_unknown_property.as_ref() { + if let Some(rule) = self.no_unknown_media_feature_name.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.no_unknown_pseudo_class_selector.as_ref() { + if let Some(rule) = self.no_unknown_property.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_class_selector.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.no_unknown_unit.as_ref() { + if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { + if let Some(rule) = self.no_unknown_unit.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.no_unused_function_parameters.as_ref() { + if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.no_useless_string_concat.as_ref() { + if let Some(rule) = self.no_unused_function_parameters.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { + if let Some(rule) = self.no_useless_string_concat.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.no_yoda_expression.as_ref() { + if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { + if let Some(rule) = self.no_yoda_expression.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { + if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.use_consistent_curly_braces.as_ref() { + if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.use_consistent_grid_areas.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.use_date_now.as_ref() { + if let Some(rule) = self.use_consistent_grid_areas.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.use_default_switch_clause.as_ref() { + if let Some(rule) = self.use_date_now.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_default_switch_clause.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.use_error_message.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } - if let Some(rule) = self.use_explicit_length_check.as_ref() { + if let Some(rule) = self.use_error_message.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - if let Some(rule) = self.use_focusable_interactive.as_ref() { + if let Some(rule) = self.use_explicit_length_check.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } - if let Some(rule) = self.use_generic_font_names.as_ref() { + if let Some(rule) = self.use_focusable_interactive.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } } - if let Some(rule) = self.use_import_extensions.as_ref() { + if let Some(rule) = self.use_generic_font_names.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_import_extensions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46])); } } - if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } } - if let Some(rule) = self.use_semantic_elements.as_ref() { + if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_semantic_elements.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + if let Some(rule) = self.use_sorted_classes.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); } } - if let Some(rule) = self.use_throw_new_error.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } } - if let Some(rule) = self.use_throw_only_error.as_ref() { + if let Some(rule) = self.use_throw_new_error.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52])); } } - if let Some(rule) = self.use_top_level_regex.as_ref() { + if let Some(rule) = self.use_throw_only_error.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_top_level_regex.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3856,6 +3871,10 @@ impl Nursery { .no_shorthand_property_overrides .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noStaticElementInteractions" => self + .no_static_element_interactions + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noSubstr" => self .no_substr .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 18368296846c..42c416eb051c 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -136,6 +136,7 @@ define_categories! { "lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword", "lint/nursery/noReactSpecificProps": "https://biomejs.dev/linter/rules/no-react-specific-props", "lint/nursery/noRestrictedImports": "https://biomejs.dev/linter/rules/no-restricted-imports", + "lint/nursery/noStaticElementInteractions": "https://biomejs.dev/linter/rules/no-static-element-interactions", "lint/nursery/noShorthandPropertyOverrides": "https://biomejs.dev/linter/rules/no-shorthand-property-overrides", "lint/nursery/noSubstr": "https://biomejs.dev/linter/rules/no-substr", "lint/nursery/noUndeclaredDependencies": "https://biomejs.dev/linter/rules/no-undeclared-dependencies", diff --git a/crates/biome_js_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs b/crates/biome_js_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs index bfa23fb2ec11..22e570f05ae5 100644 --- a/crates/biome_js_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs +++ b/crates/biome_js_analyze/src/lint/a11y/no_aria_hidden_on_focusable.rs @@ -69,6 +69,7 @@ impl Rule for NoAriaHiddenOnFocusable { let attr_text = attr_static_val.text(); let attributes = ctx.extract_attributes(&node.attributes()); + let attributes = ctx.convert_all_attribute_values(attributes); if attr_text == "false" { return None; diff --git a/crates/biome_js_analyze/src/lint/a11y/no_interactive_element_to_noninteractive_role.rs b/crates/biome_js_analyze/src/lint/a11y/no_interactive_element_to_noninteractive_role.rs index 1e97627eac31..e80af692f203 100644 --- a/crates/biome_js_analyze/src/lint/a11y/no_interactive_element_to_noninteractive_role.rs +++ b/crates/biome_js_analyze/src/lint/a11y/no_interactive_element_to_noninteractive_role.rs @@ -58,6 +58,7 @@ impl Rule for NoInteractiveElementToNoninteractiveRole { let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?; let attributes = ctx.extract_attributes(&node.attributes()); + let attributes = ctx.convert_all_attribute_values(attributes); if !aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes) && !aria_roles.is_role_interactive(role_attribute_value) { diff --git a/crates/biome_js_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs b/crates/biome_js_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs index 7ee219715b9a..c408880cb754 100644 --- a/crates/biome_js_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs +++ b/crates/biome_js_analyze/src/lint/a11y/no_noninteractive_element_to_interactive_role.rs @@ -75,6 +75,7 @@ impl Rule for NoNoninteractiveElementToInteractiveRole { let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?; let attributes = ctx.extract_attributes(&node.attributes()); + let attributes = ctx.convert_all_attribute_values(attributes); if aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes) && aria_roles.is_role_interactive(role_attribute_value) { diff --git a/crates/biome_js_analyze/src/lint/a11y/no_noninteractive_tabindex.rs b/crates/biome_js_analyze/src/lint/a11y/no_noninteractive_tabindex.rs index 1a5070e0e2d2..311600757fe1 100644 --- a/crates/biome_js_analyze/src/lint/a11y/no_noninteractive_tabindex.rs +++ b/crates/biome_js_analyze/src/lint/a11y/no_noninteractive_tabindex.rs @@ -112,6 +112,7 @@ impl Rule for NoNoninteractiveTabindex { let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?; let aria_roles = ctx.aria_roles(); let attributes = ctx.extract_attributes(&node.attributes()); + let attributes = ctx.convert_all_attribute_values(attributes); if aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes) { let tabindex_attribute = node.find_attribute_by_name("tabIndex")?; diff --git a/crates/biome_js_analyze/src/lint/a11y/no_redundant_roles.rs b/crates/biome_js_analyze/src/lint/a11y/no_redundant_roles.rs index 7fe595dc5d11..f925d0e9cd6b 100644 --- a/crates/biome_js_analyze/src/lint/a11y/no_redundant_roles.rs +++ b/crates/biome_js_analyze/src/lint/a11y/no_redundant_roles.rs @@ -70,9 +70,10 @@ impl Rule for NoRedundantRoles { let aria_roles = ctx.aria_roles(); let (element_name, attributes) = get_element_name_and_attributes(node)?; - let attribute_name_to_values = ctx.extract_attributes(&attributes)?; - let implicit_role = - aria_roles.get_implicit_role(&element_name, &attribute_name_to_values)?; + let attribute_name_to_values = ctx.extract_attributes(&attributes); + let attribute_name_to_values = ctx.convert_all_attribute_values(attribute_name_to_values); + let attr = attribute_name_to_values?; + let implicit_role = aria_roles.get_implicit_role(&element_name, &attr)?; let role_attribute = node.find_attribute_by_name("role")?; let role_attribute_value = role_attribute.initializer()?.value().ok()?; diff --git a/crates/biome_js_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rs b/crates/biome_js_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rs index 6cddba586091..ef3389f422a6 100644 --- a/crates/biome_js_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rs +++ b/crates/biome_js_analyze/src/lint/a11y/use_aria_activedescendant_with_tabindex.rs @@ -69,6 +69,7 @@ impl Rule for UseAriaActivedescendantWithTabindex { let aria_roles = ctx.aria_roles(); let element_name = node.name().ok()?.as_jsx_name()?.value_token().ok()?; let attributes = ctx.extract_attributes(&node.attributes()); + let attributes = ctx.convert_all_attribute_values(attributes); if node.is_element() && aria_roles.is_not_interactive_element(element_name.text_trimmed(), attributes) diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 584a7d0114bb..b67667ecb6c3 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -13,6 +13,7 @@ pub mod no_label_without_control; pub mod no_misplaced_assertion; pub mod no_react_specific_props; pub mod no_restricted_imports; +pub mod no_static_element_interactions; pub mod no_substr; pub mod no_undeclared_dependencies; pub mod no_unused_function_parameters; @@ -53,6 +54,7 @@ declare_lint_group! { self :: no_misplaced_assertion :: NoMisplacedAssertion , self :: no_react_specific_props :: NoReactSpecificProps , self :: no_restricted_imports :: NoRestrictedImports , + self :: no_static_element_interactions :: NoStaticElementInteractions , self :: no_substr :: NoSubstr , self :: no_undeclared_dependencies :: NoUndeclaredDependencies , self :: no_unused_function_parameters :: NoUnusedFunctionParameters , diff --git a/crates/biome_js_analyze/src/lint/nursery/no_static_element_interactions.rs b/crates/biome_js_analyze/src/lint/nursery/no_static_element_interactions.rs new file mode 100644 index 000000000000..bbd6b20bbf36 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_static_element_interactions.rs @@ -0,0 +1,166 @@ +use crate::services::aria::Aria; +use biome_analyze::context::RuleContext; +use biome_analyze::{declare_lint_rule, Rule, RuleDiagnostic, RuleSource}; +use biome_console::markup; +use biome_js_syntax::jsx_ext::AnyJsxElement; +use biome_rowan::AstNode; + +declare_lint_rule! { + /// Enforce that static, visible elements (such as `
`) that have click handlers use the valid role attribute. + /// + /// Static HTML elements do not have semantic meaning. This is clear in the case of `
` and ``. It is less so clear in the case of elements that seem semantic, but that do not have a semantic mapping in the accessibility layer. For example `` without href attribute, ``, `