mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
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:
parent
7ec0c0bdd4
commit
278c3e9313
4 changed files with 83 additions and 56 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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! {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue