From 6ca13683bab5707ae819db67ce5af6e5428af400 Mon Sep 17 00:00:00 2001 From: Billy Levin Date: Sat, 24 Aug 2024 08:48:19 +0100 Subject: [PATCH 1/4] (feat/jsx-a11y): add label-has-associated-control rule --- Cargo.lock | 1 + crates/oxc_linter/Cargo.toml | 1 + crates/oxc_linter/src/rules.rs | 2 + .../jsx_a11y/label_has_associated_control.rs | 1560 +++++++++++++++++ .../label_has_associated_control.snap | 610 +++++++ 5 files changed, 2174 insertions(+) create mode 100644 crates/oxc_linter/src/rules/jsx_a11y/label_has_associated_control.rs create mode 100644 crates/oxc_linter/src/snapshots/label_has_associated_control.snap diff --git a/Cargo.lock b/Cargo.lock index 68276166a123e..dce19ca81881f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1594,6 +1594,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 60e000f7d7dec..b906ed6146fb6 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 } [dev-dependencies] insta = { workspace = true } diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 95075ac8e1ae7..9ea0f38511935 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; @@ -792,6 +793,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..3711367c0eca4 --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/label_has_associated_control.rs @@ -0,0 +1,1560 @@ +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::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` atrribute 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)] +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 Default for LabelHasAssociatedControlConfig { + fn default() -> Self { + Self { + depth: 2, + assert: Assert::Either, + label_components: vec!["label".to_string()], + label_attributes: vec![ + "alt".to_string(), + "aria-label".to_string(), + "aria-labelledby".to_string(), + ], + 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