diff --git a/Cargo.lock b/Cargo.lock index d006fd85fef8e..55ce9018eac1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1609,6 +1609,7 @@ dependencies = [ "bitflags 2.6.0", "convert_case", "dashmap 6.0.1", + "globset", "insta", "itertools", "json-strip-comments", diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index b1399716ae652..d0e4c374c7c3d 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -52,6 +52,7 @@ once_cell = { workspace = true } memchr = { workspace = true } json-strip-comments = { workspace = true } schemars = { workspace = true, features = ["indexmap2"] } +globset = { workspace = true } simdutf8 = { workspace = true } [dev-dependencies] diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index aecdf477c82a8..7331564ddfc14 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -357,6 +357,7 @@ mod jsx_a11y { pub mod html_has_lang; pub mod iframe_has_title; pub mod img_redundant_alt; + pub mod label_has_associated_control; pub mod lang; pub mod media_has_caption; pub mod mouse_events_have_key_events; @@ -793,6 +794,7 @@ oxc_macros::declare_all_lint_rules! { jsx_a11y::lang, jsx_a11y::iframe_has_title, jsx_a11y::img_redundant_alt, + jsx_a11y::label_has_associated_control, jsx_a11y::media_has_caption, jsx_a11y::mouse_events_have_key_events, jsx_a11y::no_access_key, diff --git a/crates/oxc_linter/src/rules/jsx_a11y/label_has_associated_control.rs b/crates/oxc_linter/src/rules/jsx_a11y/label_has_associated_control.rs new file mode 100644 index 0000000000000..369184f558906 --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/label_has_associated_control.rs @@ -0,0 +1,1591 @@ +use std::ops::Deref; + +use globset::{Glob, GlobSet, GlobSetBuilder}; +use oxc_ast::{ + ast::{JSXAttributeItem, JSXAttributeValue, JSXChild, JSXElement}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, Span}; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{get_element_type, get_jsx_attribute_name, has_jsx_prop, is_react_component_name}, + AstNode, +}; + +fn label_has_associated_control_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("A form label must be associated with a control.") + .with_help("Either give the label a `htmlFor` attribute with the id of the associated control, or wrap the label around the control.") + .with_label(span0) +} + +fn label_has_associated_control_diagnostic_no_label(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("A form label must have accessible text.") + .with_help("Ensure the label either has text inside it or is accessibly labelled using an attribute such as `aria-label`, or `aria-labelledby`. You can mark more attributes as accessible labels by configuring the `labelAttributes` option.") + .with_label(span0) +} + +#[derive(Debug, Default, Clone)] +pub struct LabelHasAssociatedControl(Box); + +#[derive(Debug, Clone)] +pub struct LabelHasAssociatedControlConfig { + depth: u8, + assert: Assert, + label_components: Vec, + label_attributes: Vec, + control_components: GlobSet, +} + +#[derive(Debug, Clone)] +enum Assert { + HtmlFor, + Nesting, + Both, + Either, +} + +impl Deref for LabelHasAssociatedControl { + type Target = LabelHasAssociatedControlConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Default for LabelHasAssociatedControlConfig { + fn default() -> Self { + Self { + depth: 2, + assert: Assert::Either, + label_components: vec!["label".into()], + label_attributes: vec!["alt".into(), "aria-label".into(), "aria-labelledby".into()], + control_components: GlobSet::empty(), + } + } +} + +declare_oxc_lint!( + /// ### What it does + /// Enforce that a label tag has a text label and an associated control. + /// + /// ### Why is this bad? + /// A form label that either isn't properly associated with a form control (such as an ``), or doesn't contain accessible text, hinders accessibility for users using assistive technologies such as screen readers. The user may not have enough information to understand the purpose of the form control. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```jsx + /// function Foo(props) { + /// return