feat(linter): add fixer for jsx-a11y/aria-props (#4176)

Part of  #4179.

Adds a fixer for some common typos.
This commit is contained in:
DonIsaac 2024-07-11 04:33:37 +00:00
parent 7ec0c0bdd4
commit 278c3e9313
4 changed files with 83 additions and 56 deletions

View file

@ -7,6 +7,7 @@ extend-exclude = [
"**/*.snap", "**/*.snap",
"**/*/CHANGELOG.md", "**/*/CHANGELOG.md",
"crates/oxc_linter/fixtures", "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/jsx_a11y/img_redundant_alt.rs",
"crates/oxc_linter/src/rules/react/no_unknown_property.rs", "crates/oxc_linter/src/rules/react/no_unknown_property.rs",
"crates/oxc_parser/src/lexer/byte_handlers.rs", "crates/oxc_parser/src/lexer/byte_handlers.rs",

View file

@ -91,12 +91,54 @@ pub const VALID_ARIA_ROLES: phf::Set<&'static str> = phf_set! {
"deletion", "deletion",
"dialog", "dialog",
"directory", "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", "document",
"emphasis", "emphasis",
"feed", "feed",
"figure", "figure",
"form", "form",
"generic", "generic",
"graphics-document",
"graphics-object",
"graphics-symbol",
"grid", "grid",
"gridcell", "gridcell",
"group", "group",
@ -154,49 +196,7 @@ pub const VALID_ARIA_ROLES: phf::Set<&'static str> = phf_set! {
"tooltip", "tooltip",
"tree", "tree",
"treegrid", "treegrid",
"treeitem", "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"
}; };
pub const HTML_TAG: phf::Set<&'static str> = phf_set! { pub const HTML_TAG: phf::Set<&'static str> = phf_set! {

View file

@ -1,17 +1,23 @@
use oxc_ast::{ast::JSXAttributeItem, AstKind}; use oxc_ast::{ast::JSXAttributeItem, AstKind};
use oxc_diagnostics::OxcDiagnostic; use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint; use oxc_macros::declare_oxc_lint;
use oxc_span::Span; use oxc_span::{GetSpan, Span};
use crate::{ use crate::{
context::LintContext, globals::VALID_ARIA_PROPS, rule::Rule, utils::get_jsx_attribute_name, context::LintContext, globals::VALID_ARIA_PROPS, rule::Rule, utils::get_jsx_attribute_name,
AstNode, AstNode,
}; };
fn aria_props_diagnostic(span0: Span, x1: &str) -> OxcDiagnostic { fn aria_props_diagnostic(span: Span, prop_name: &str, suggestion: Option<&str>) -> OxcDiagnostic {
OxcDiagnostic::warn("eslint-plugin-jsx-a11y(aria-props): Invalid ARIA prop.") let mut err = OxcDiagnostic::warn(format!(
.with_help(format!("`{x1}` is an invalid ARIA attribute.")) "eslint-plugin-jsx-a11y(aria-props): '{prop_name}' is not a valid ARIA attribute."
.with_label(span0) ));
if let Some(suggestion) = suggestion {
err = err.with_help(format!("Did you mean '{suggestion}'?"));
}
err.with_label(span)
} }
#[derive(Debug, Default, Clone)] #[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 /// It may cause the accessibility features of the website to fail, making it difficult
/// for users with disabilities to use the site effectively. /// for users with disabilities to use the site effectively.
/// ///
/// This rule includes fixes for some common typos.
///
/// ### Example /// ### Example
/// ```javascript /// ```javascript
/// // Bad /// // Bad
@ -37,17 +45,35 @@ declare_oxc_lint!(
AriaProps, AriaProps,
correctness correctness
); );
impl Rule for AriaProps { impl Rule for AriaProps {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXAttributeItem(JSXAttributeItem::Attribute(attr)) = node.kind() { if let AstKind::JSXAttributeItem(JSXAttributeItem::Attribute(attr)) = node.kind() {
let name = get_jsx_attribute_name(&attr.name).to_lowercase(); let name = get_jsx_attribute_name(&attr.name).to_lowercase();
if name.starts_with("aria-") && !VALID_ARIA_PROPS.contains(&name) { 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] #[test]
fn test() { fn test() {
use crate::tester::Tester; use crate::tester::Tester;
@ -68,6 +94,8 @@ fn test() {
r#"<div aria-labeledby="foobar" />"#, r#"<div aria-labeledby="foobar" />"#,
r#"<div aria-skldjfaria-klajsd="foobar" />"#, r#"<div aria-skldjfaria-klajsd="foobar" />"#,
]; ];
let fix =
vec![(r#"<div aria-labeledby="foobar" />"#, r#"<div aria-labelledby="foobar" />"#, None)];
Tester::new(AriaProps::NAME, pass, fail).test_and_snapshot(); Tester::new(AriaProps::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
} }

View file

@ -1,23 +1,21 @@
--- ---
source: crates/oxc_linter/src/tester.rs 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] ╭─[aria_props.tsx:1:6]
1 │ <div aria-="foobar" /> 1 │ <div aria-="foobar" />
· ────────────── · ──────────────
╰──── ╰────
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] ╭─[aria_props.tsx:1:6]
1 │ <div aria-labeledby="foobar" /> 1 │ <div aria-labeledby="foobar" />
· ─────────────────────── · ───────────────────────
╰──── ╰────
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] ╭─[aria_props.tsx:1:6]
1 │ <div aria-skldjfaria-klajsd="foobar" /> 1 │ <div aria-skldjfaria-klajsd="foobar" />
· ─────────────────────────────── · ───────────────────────────────
╰──── ╰────
help: `aria-skldjfaria-klajsd` is an invalid ARIA attribute.