diff --git a/crates/oxc_linter/src/fixer/fix.rs b/crates/oxc_linter/src/fixer/fix.rs index 8296e3201d878..6e868ce1f1d32 100644 --- a/crates/oxc_linter/src/fixer/fix.rs +++ b/crates/oxc_linter/src/fixer/fix.rs @@ -145,6 +145,16 @@ macro_rules! impl_from { // but this breaks when implementing `From> for CompositeFix<'a>`. impl_from!(CompositeFix<'a>, Fix<'a>, Option>, Vec>); +impl<'a> FromIterator> for RuleFix<'a> { + fn from_iter>>(iter: T) -> Self { + Self { + kind: FixKind::SafeFix, + message: None, + fix: iter.into_iter().collect::>().into(), + } + } +} + impl<'a> From> for CompositeFix<'a> { #[inline] fn from(val: RuleFix<'a>) -> Self { diff --git a/crates/oxc_linter/src/rules/jsx_a11y/anchor_has_content.rs b/crates/oxc_linter/src/rules/jsx_a11y/anchor_has_content.rs index 15c81fd6c389b..5ab1d294b36f4 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/anchor_has_content.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/anchor_has_content.rs @@ -1,10 +1,14 @@ -use oxc_ast::AstKind; +use oxc_ast::{ + ast::{JSXAttributeItem, JSXChild, JSXElement}, + AstKind, +}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_span::Span; use crate::{ context::LintContext, + fixer::{Fix, RuleFix}, rule::Rule, utils::{ get_element_type, has_jsx_prop_ignore_case, is_hidden_from_screen_reader, @@ -19,12 +23,6 @@ fn missing_content(span0: Span) -> OxcDiagnostic { .with_label(span0) } -fn remove_aria_hidden(span0: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Missing accessible content when using `a` elements.") - .with_help("Remove the `aria-hidden` attribute to allow the anchor element and its content visible to assistive technologies.") - .with_label(span0) -} - #[derive(Debug, Default, Clone)] pub struct AnchorHasContent; @@ -59,7 +57,8 @@ declare_oxc_lint!( /// ``` /// AnchorHasContent, - correctness + correctness, + conditional_suggestion ); impl Rule for AnchorHasContent { @@ -70,7 +69,9 @@ impl Rule for AnchorHasContent { }; if name == "a" { if is_hidden_from_screen_reader(ctx, &jsx_el.opening_element) { - ctx.diagnostic(remove_aria_hidden(jsx_el.span)); + // ctx.diagnostic_with_suggestion(remove_aria_hidden(jsx_el.span), |_fixer| { + // remove_hidden_attributes(&jsx_el.opening_element) + // }); return; } @@ -84,12 +85,43 @@ impl Rule for AnchorHasContent { }; } - ctx.diagnostic(missing_content(jsx_el.span)); + let diagnostic = missing_content(jsx_el.span); + if jsx_el.children.len() == 1 { + let child = &jsx_el.children[0]; + if let JSXChild::Element(child) = child { + ctx.diagnostic_with_suggestion(diagnostic, |_fixer| { + remove_hidden_attributes(child) + }); + return; + } + } + + ctx.diagnostic(diagnostic); } } } } +fn remove_hidden_attributes<'a>(element: &JSXElement<'a>) -> RuleFix<'a> { + element + .opening_element + .attributes + .iter() + .filter_map(JSXAttributeItem::as_attribute) + .filter_map(|attr| { + attr.name.as_identifier().and_then(|name| { + if name.name.eq_ignore_ascii_case("aria-hidden") + || name.name.eq_ignore_ascii_case("hidden") + { + Some(Fix::delete(attr.span)) + } else { + None + } + }) + }) + .collect() +} + #[test] fn test() { use crate::tester::Tester; @@ -114,12 +146,27 @@ fn test() { (r"", None, None), (r"", None, None), (r"", None, None), + (r#""#, None, None), + // anchors can be hidden + (r"Foo", None, None), + (r#""#, None, None), + (r"", None, None), + (r"Foo", None, None), + (r#""#, None, None), + (r#""#, None, None), + // TODO: should these be failing? + (r"", None, None), + (r#""#, None, None), + (r#""#, None, None), ]; let fail = vec![ (r"", None, None), (r"", None, None), + (r#""#, None, None), + (r#""#, None, None), (r"{undefined}", None, None), + (r"{null}", None, None), ( r"", None, @@ -129,5 +176,15 @@ fn test() { ), ]; - Tester::new(AnchorHasContent::NAME, pass, fail).test_and_snapshot(); + let fix = vec![ + (r"", ""), + (r"Can't see me", r"Can't see me"), + (r"Can't see me", r"Can't see me"), + ( + r#""#, + r"Can't see me", + ), + ]; + + Tester::new(AnchorHasContent::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); } diff --git a/crates/oxc_linter/src/snapshots/anchor_has_content.snap b/crates/oxc_linter/src/snapshots/anchor_has_content.snap index f1f3e999849d0..1b95f380f53cf 100644 --- a/crates/oxc_linter/src/snapshots/anchor_has_content.snap +++ b/crates/oxc_linter/src/snapshots/anchor_has_content.snap @@ -15,6 +15,20 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Provide screen reader accessible content when using `a` elements. + ⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements. + ╭─[anchor_has_content.tsx:1:1] + 1 │ + · ───────────────────────────────── + ╰──── + help: Provide screen reader accessible content when using `a` elements. + + ⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements. + ╭─[anchor_has_content.tsx:1:1] + 1 │ + · ────────────────────────────── + ╰──── + help: Provide screen reader accessible content when using `a` elements. + ⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements. ╭─[anchor_has_content.tsx:1:1] 1 │ {undefined} @@ -22,6 +36,13 @@ source: crates/oxc_linter/src/tester.rs ╰──── help: Provide screen reader accessible content when using `a` elements. + ⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements. + ╭─[anchor_has_content.tsx:1:1] + 1 │ {null} + · ───────────── + ╰──── + help: Provide screen reader accessible content when using `a` elements. + ⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing accessible content when using `a` elements. ╭─[anchor_has_content.tsx:1:1] 1 │