diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 1211b96da809a..5b2ef4846bae2 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -226,6 +226,7 @@ mod jsx_a11y { pub mod html_has_lang; pub mod iframe_has_title; pub mod img_redundant_alt; + pub mod scope; } mod oxc { @@ -431,5 +432,6 @@ oxc_macros::declare_all_lint_rules! { jsx_a11y::html_has_lang, jsx_a11y::iframe_has_title, jsx_a11y::img_redundant_alt, + jsx_a11y::scope, oxc::no_accumulating_spread } diff --git a/crates/oxc_linter/src/rules/jsx_a11y/scope.rs b/crates/oxc_linter/src/rules/jsx_a11y/scope.rs new file mode 100644 index 0000000000000..140dd450f0316 --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/scope.rs @@ -0,0 +1,100 @@ +use oxc_ast::{ + ast::{JSXAttributeItem, JSXElementName}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, utils::has_jsx_prop_lowercase, AstNode}; + +#[derive(Debug, Default, Clone)] +pub struct Scope; + +declare_oxc_lint!( + /// ### What it does + /// + /// The scope prop should be used only on elements. + /// + /// ### Why is this bad? + /// The scope attribute makes table navigation much easier for screen reader users, provided that it is used correctly. + /// Incorrectly used, scope can make table navigation much harder and less efficient. + /// A screen reader operates under the assumption that a table has a header and that this header specifies a scope. Because of the way screen readers function, having an accurate header makes viewing a table far more accessible and more efficient for people who use the device. + /// + /// ### Example + /// ```javascript + /// // Bad + ///
+ /// + /// // Good + /// + /// + /// ``` + Scope, + correctness +); + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-jsx-a11y(scope): The scope prop can only be used on elements")] +#[diagnostic(severity(warning), help("Must use scope prop only on elements"))] +struct ScopeDiagnostic(#[label] pub Span); + +impl Rule for Scope { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { + return; + }; + + let scope_attribute = match has_jsx_prop_lowercase(jsx_el, "scope") { + Some(v) => match v { + JSXAttributeItem::Attribute(attr) => attr, + JSXAttributeItem::SpreadAttribute(_) => { + return; + } + }, + None => { + return; + } + }; + + let JSXElementName::Identifier(identifier) = &jsx_el.name else { + return; + }; + + let name = identifier.name.as_str(); + if name == "th" { + return; + } + + ctx.diagnostic(ScopeDiagnostic(scope_attribute.span)); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + (r"
;", None), + (r"
;", None), + (r"", None), + (r"", None), + (r"", None), + (r"", None), + // TODO aria-query like parts is needed + // (r"", None), + // TODO: When polymorphic components are supported + // (r"", None) + ]; + + let fail = vec![ + (r"
", None), + // TODO: When polymorphic components are supported + // (r";", None), + ]; + + Tester::new(Scope::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/scope.snap b/crates/oxc_linter/src/snapshots/scope.snap new file mode 100644 index 0000000000000..49467bc6dd80b --- /dev/null +++ b/crates/oxc_linter/src/snapshots/scope.snap @@ -0,0 +1,12 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: scope +--- + ⚠ eslint-plugin-jsx-a11y(scope): The scope prop can only be used on elements + ╭─[scope.tsx:1:1] + 1 │
+ · ───── + ╰──── + help: Must use scope prop only on elements + +