diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 699a16a17ad14..3944cf3c0774a 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -231,6 +231,7 @@ mod jsx_a11y { pub mod img_redundant_alt; pub mod no_autofocus; pub mod scope; + pub mod tab_index_no_positive; } mod oxc { @@ -439,7 +440,8 @@ oxc_macros::declare_all_lint_rules! { jsx_a11y::html_has_lang, jsx_a11y::iframe_has_title, jsx_a11y::img_redundant_alt, - jsx_a11y::scope, jsx_a11y::no_autofocus, + jsx_a11y::scope, + jsx_a11y::tab_index_no_positive, oxc::no_accumulating_spread } diff --git a/crates/oxc_linter/src/rules/jsx_a11y/tab_index_no_positive.rs b/crates/oxc_linter/src/rules/jsx_a11y/tab_index_no_positive.rs new file mode 100644 index 0000000000000..815caea35a5bc --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/tab_index_no_positive.rs @@ -0,0 +1,118 @@ +use oxc_ast::{ + ast::{Expression, JSXAttributeItem, JSXAttributeValue, JSXExpression, JSXExpressionContainer}, + 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, Error, Diagnostic)] +#[error( + "eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex." +)] +#[diagnostic(severity(warning), help("Change the tabIndex prop to a non-negative value"))] +struct TabIndexNoPositiveDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct TabIndexNoPositive; + +declare_oxc_lint!( + /// ### What it does + /// Enforces that positive values for the tabIndex attribute are not used in JSX. + /// + /// ### Why is this bad? + /// Using tabIndex values greater than 0 can make navigation and interaction difficult for keyboard and assistive technology users, disrupting the logical order of content. + /// + /// ### Example + /// ```javascript + /// // Bad + /// foo + /// + /// // Good + /// foo + /// bar + /// ``` + TabIndexNoPositive, + correctness +); + +impl Rule for TabIndexNoPositive { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { return }; + if let Some(tab_index_prop) = has_jsx_prop_lowercase(jsx_el, "tabIndex") { + check_and_diagnose(tab_index_prop, ctx); + } + } +} + +fn check_and_diagnose(attr: &JSXAttributeItem, ctx: &LintContext<'_>) { + match attr { + JSXAttributeItem::Attribute(attr) => attr.value.as_ref().map_or((), |value| { + if let Ok(parsed_value) = parse_jsx_value(value) { + if parsed_value > 0.0 { + ctx.diagnostic(TabIndexNoPositiveDiagnostic(attr.span)); + } + } + }), + JSXAttributeItem::SpreadAttribute(_) => {} + } +} + +fn parse_jsx_value(value: &JSXAttributeValue) -> Result { + match value { + JSXAttributeValue::StringLiteral(str) => str.value.parse().or(Err(())), + JSXAttributeValue::ExpressionContainer(JSXExpressionContainer { + expression: JSXExpression::Expression(expression), + .. + }) => match expression { + Expression::StringLiteral(str) => str.value.parse().or(Err(())), + Expression::TemplateLiteral(tmpl) => { + tmpl.quasis.get(0).unwrap().value.raw.parse().or(Err(())) + } + Expression::NumberLiteral(num) => Ok(num.value), + _ => Err(()), + }, + _ => Err(()), + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + (r"
;", None), + (r"
", None), + (r#"
"#, None), + (r"
", None), + (r"
", None), + (r"
", None), + (r"
", None), + (r"
", None), + (r"
", None), + (r"
", None), + (r"
", None), + (r#"
"#, None), + (r#"
"#, None), + (r#"
"#, None), + (r#"
"#, None), + (r#"
"#, None), + (r"
", None), + (r"
", None), + ]; + + let fail = vec![ + (r#"
"#, None), + (r"
", None), + (r#"
"#, None), + (r"
", None), + (r"
", None), + ]; + + Tester::new(TabIndexNoPositive::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/tab_index_no_positive.snap b/crates/oxc_linter/src/snapshots/tab_index_no_positive.snap new file mode 100644 index 0000000000000..ee2b148b66b51 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/tab_index_no_positive.snap @@ -0,0 +1,40 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: tab_index_no_positive +--- + ⚠ eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex. + ╭─[tab_index_no_positive.tsx:1:1] + 1 │
+ · ──────────── + ╰──── + help: Change the tabIndex prop to a non-negative value + + ⚠ eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex. + ╭─[tab_index_no_positive.tsx:1:1] + 1 │
+ · ──────────── + ╰──── + help: Change the tabIndex prop to a non-negative value + + ⚠ eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex. + ╭─[tab_index_no_positive.tsx:1:1] + 1 │
+ · ────────────── + ╰──── + help: Change the tabIndex prop to a non-negative value + + ⚠ eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex. + ╭─[tab_index_no_positive.tsx:1:1] + 1 │
+ · ────────────── + ╰──── + help: Change the tabIndex prop to a non-negative value + + ⚠ eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex. + ╭─[tab_index_no_positive.tsx:1:1] + 1 │
+ · ──────────────── + ╰──── + help: Change the tabIndex prop to a non-negative value + +