diff --git a/.typos.toml b/.typos.toml index 233c0fc41cbad..28aa370e20bff 100644 --- a/.typos.toml +++ b/.typos.toml @@ -7,6 +7,7 @@ extend-exclude = [ "**/*.snap", "**/*/CHANGELOG.md", "crates/oxc_linter/fixtures", + "crates/oxc_linter/src/rules/jsx_a11y/aria_props.rs", "crates/oxc_linter/src/rules/jsx_a11y/img_redundant_alt.rs", "crates/oxc_linter/src/rules/react/no_unknown_property.rs", "crates/oxc_parser/src/lexer/byte_handlers.rs", diff --git a/crates/oxc_linter/src/globals.rs b/crates/oxc_linter/src/globals.rs index 6deae467b0cca..2b0ed49767926 100644 --- a/crates/oxc_linter/src/globals.rs +++ b/crates/oxc_linter/src/globals.rs @@ -91,12 +91,54 @@ pub const VALID_ARIA_ROLES: phf::Set<&'static str> = phf_set! { "deletion", "dialog", "directory", + "doc-abstract", + "doc-acknowledgments", + "doc-afterword", + "doc-appendix", + "doc-backlink", + "doc-biblioentry", + "doc-bibliography", + "doc-biblioref", + "doc-chapter", + "doc-colophon", + "doc-conclusion", + "doc-cover", + "doc-credit", + "doc-credits", + "doc-dedication", + "doc-endnote", + "doc-endnotes", + "doc-epigraph", + "doc-epilogue", + "doc-errata", + "doc-example", + "doc-footnote", + "doc-foreword", + "doc-glossary", + "doc-glossref", + "doc-index", + "doc-introduction", + "doc-noteref", + "doc-notice", + "doc-pagebreak", + "doc-pagelist", + "doc-part", + "doc-preface", + "doc-prologue", + "doc-pullquote", + "doc-qna", + "doc-subtitle", + "doc-tip", + "doc-toc", "document", "emphasis", "feed", "figure", "form", "generic", + "graphics-document", + "graphics-object", + "graphics-symbol", "grid", "gridcell", "group", @@ -154,49 +196,7 @@ pub const VALID_ARIA_ROLES: phf::Set<&'static str> = phf_set! { "tooltip", "tree", "treegrid", - "treeitem", - "doc-abstract", - "doc-acknowledgments", - "doc-afterword", - "doc-appendix", - "doc-backlink", - "doc-biblioentry", - "doc-bibliography", - "doc-biblioref", - "doc-chapter", - "doc-colophon", - "doc-conclusion", - "doc-cover", - "doc-credit", - "doc-credits", - "doc-dedication", - "doc-endnote", - "doc-endnotes", - "doc-epigraph", - "doc-epilogue", - "doc-errata", - "doc-example", - "doc-footnote", - "doc-foreword", - "doc-glossary", - "doc-glossref", - "doc-index", - "doc-introduction", - "doc-noteref", - "doc-notice", - "doc-pagebreak", - "doc-pagelist", - "doc-part", - "doc-preface", - "doc-prologue", - "doc-pullquote", - "doc-qna", - "doc-subtitle", - "doc-tip", - "doc-toc", - "graphics-document", - "graphics-object", - "graphics-symbol" + "treeitem" }; pub const HTML_TAG: phf::Set<&'static str> = phf_set! { diff --git a/crates/oxc_linter/src/rules/jsx_a11y/aria_props.rs b/crates/oxc_linter/src/rules/jsx_a11y/aria_props.rs index b6b1ee69c9f0a..0c310df47c331 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/aria_props.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/aria_props.rs @@ -1,17 +1,23 @@ use oxc_ast::{ast::JSXAttributeItem, AstKind}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +use oxc_span::{GetSpan, Span}; use crate::{ context::LintContext, globals::VALID_ARIA_PROPS, rule::Rule, utils::get_jsx_attribute_name, AstNode, }; -fn aria_props_diagnostic(span0: Span, x1: &str) -> OxcDiagnostic { - OxcDiagnostic::warn("eslint-plugin-jsx-a11y(aria-props): Invalid ARIA prop.") - .with_help(format!("`{x1}` is an invalid ARIA attribute.")) - .with_label(span0) +fn aria_props_diagnostic(span: Span, prop_name: &str, suggestion: Option<&str>) -> OxcDiagnostic { + let mut err = OxcDiagnostic::warn(format!( + "eslint-plugin-jsx-a11y(aria-props): '{prop_name}' is not a valid ARIA attribute." + )); + + if let Some(suggestion) = suggestion { + err = err.with_help(format!("Did you mean '{suggestion}'?")); + } + + err.with_label(span) } #[derive(Debug, Default, Clone)] @@ -26,6 +32,8 @@ declare_oxc_lint!( /// It may cause the accessibility features of the website to fail, making it difficult /// for users with disabilities to use the site effectively. /// + /// This rule includes fixes for some common typos. + /// /// ### Example /// ```javascript /// // Bad @@ -37,17 +45,35 @@ declare_oxc_lint!( AriaProps, correctness ); + impl Rule for AriaProps { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { if let AstKind::JSXAttributeItem(JSXAttributeItem::Attribute(attr)) = node.kind() { let name = get_jsx_attribute_name(&attr.name).to_lowercase(); if name.starts_with("aria-") && !VALID_ARIA_PROPS.contains(&name) { - ctx.diagnostic(aria_props_diagnostic(attr.span, &name)); + let suggestion = COMMON_TYPOS.get(&name).copied(); + let diagnostic = aria_props_diagnostic(attr.span, &name, suggestion); + + if let Some(suggestion) = suggestion { + ctx.diagnostic_with_fix(diagnostic, |fixer| { + fixer.replace(attr.name.span(), suggestion) + }); + } else { + ctx.diagnostic(diagnostic); + } } } } } +const COMMON_TYPOS: phf::Map<&'static str, &'static str> = phf::phf_map! { + "aria-labeledby" => "aria-labelledby", + "aria-role" => "role", + "aria-sorted" => "aria-sort", + "aria-lable" => "aria-label", + "aria-value" => "aria-valuenow", +}; + #[test] fn test() { use crate::tester::Tester; @@ -68,6 +94,8 @@ fn test() { r#"
"#, r#""#, ]; + let fix = + vec![(r#""#, r#""#, None)]; - Tester::new(AriaProps::NAME, pass, fail).test_and_snapshot(); + Tester::new(AriaProps::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); } diff --git a/crates/oxc_linter/src/snapshots/aria_props.snap b/crates/oxc_linter/src/snapshots/aria_props.snap index 13a60f631b394..296fd390fe353 100644 --- a/crates/oxc_linter/src/snapshots/aria_props.snap +++ b/crates/oxc_linter/src/snapshots/aria_props.snap @@ -1,23 +1,21 @@ --- source: crates/oxc_linter/src/tester.rs --- - ⚠ eslint-plugin-jsx-a11y(aria-props): Invalid ARIA prop. + ⚠ eslint-plugin-jsx-a11y(aria-props): 'aria-' is not a valid ARIA attribute. ╭─[aria_props.tsx:1:6] 1 │ · ────────────── ╰──── - help: `aria-` is an invalid ARIA attribute. - ⚠ eslint-plugin-jsx-a11y(aria-props): Invalid ARIA prop. + ⚠ eslint-plugin-jsx-a11y(aria-props): 'aria-labeledby' is not a valid ARIA attribute. ╭─[aria_props.tsx:1:6] 1 │ · ─────────────────────── ╰──── - help: `aria-labeledby` is an invalid ARIA attribute. + help: Did you mean 'aria-labelledby'? - ⚠ eslint-plugin-jsx-a11y(aria-props): Invalid ARIA prop. + ⚠ eslint-plugin-jsx-a11y(aria-props): 'aria-skldjfaria-klajsd' is not a valid ARIA attribute. ╭─[aria_props.tsx:1:6] 1 │ · ─────────────────────────────── ╰──── - help: `aria-skldjfaria-klajsd` is an invalid ARIA attribute.