fix(linter/jsx_a11y): Ensure plugin settings are used (#2359)

Currently, some of the rules did not use `settings`, so make sure they
do.

- [x] Align implementation of `getElementType()`
  - (This is the only util that depends on `settings`)

Original rules which use `getElementType()` are:

- [x] 🙅🏻‍♂️ accessible-emoji
- [x]  alt-text
- [x] 🙅🏻‍♂️ anchor-ambiguous-text
- [x]  anchor-has-content
- [x] anchor-is-valid
  - TODO: `from_configuration()` not implemented => #2361 
- [x] 🙆🏻‍♀️ aria-activedescendant-has-tabindex
- [x] 🙆🏻‍♀️ aria-role
- [x] 🙆🏻‍♀️ aria-unsupported-elements
- [x] autocomplete-valid
- TODO: `from_configuration()` not implemented and needed for this =>
#2362
- [x] 🙆🏻‍♀️ click-events-have-key-events
- [x] 🙅🏻‍♂️ control-has-associated-label
- [x] heading-has-content
  - TODO: 1 test should be failed but passes 🤔 => #2360 
- [x] 🙆🏻‍♀️ html-has-lang
- [x]  iframe-has-title
- [x] 🙆🏻‍♀️ img-redundant-alt
- [x] 🙅🏻‍♂️ interactive-supports-focus
- [x] 🙅🏻‍♂️ label-has-associated-control
- [x] 🙅🏻‍♂️ label-has-for
- [x] 🙆🏻‍♀️ lang
- [x] 🙆🏻‍♀️ media-has-caption
- [x]  no-aria-hidden-on-focusable
- [x] 🙆🏻‍♀️ no-autofocus
- [x] 🙆🏻‍♀️ no-distracting-elements
- [x] 🙅🏻‍♂️ no-interactive-element-to-noninteractive-role
- [x] 🙅🏻‍♂️ no-noninteractive-element-interactions
- [x] 🙅🏻‍♂️ no-noninteractive-element-to-interactive-role
- [x] 🙅🏻‍♂️ no-noninteractive-tabindex
- [x] 🙅🏻‍♂️ no-onchange
- [x] 🙆🏻‍♀️ no-redundant-roles
- [x] 🙅🏻‍♂️ no-static-element-interactions
- [x]  prefer-tag-over-role
- [x] 🙆🏻‍♀️ role-has-required-aria-props
- [x] 🙆🏻‍♀️ role-supports-aria-props
- [x] 🙆🏻‍♀️ scope

🙅🏻‍♂️ = Not implemented yet by oxlint /  = Fixed by this PR / 🙆🏻‍♀️ =
Already used
This commit is contained in:
Yuji Sugiura 2024-02-09 21:55:50 +09:00 committed by GitHub
parent 2f6cf73d51
commit 2521b52011
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 433 additions and 334 deletions

View file

@ -1,6 +1,6 @@
use oxc_ast::{ use oxc_ast::{
ast::{ ast::{
JSXAttributeItem, JSXAttributeValue, JSXChild, JSXElement, JSXElementName, JSXExpression, JSXAttributeItem, JSXAttributeValue, JSXChild, JSXElement, JSXExpression,
JSXExpressionContainer, JSXOpeningElement, JSXExpressionContainer, JSXOpeningElement,
}, },
AstKind, AstKind,
@ -12,7 +12,9 @@ use oxc_diagnostics::{
use oxc_macros::declare_oxc_lint; use oxc_macros::declare_oxc_lint;
use oxc_span::Span; use oxc_span::Span;
use crate::utils::{get_prop_value, get_string_literal_prop_value, has_jsx_prop_lowercase}; use crate::utils::{
get_element_type, get_prop_value, get_string_literal_prop_value, has_jsx_prop_lowercase,
};
use crate::{context::LintContext, rule::Rule, AstNode}; use crate::{context::LintContext, rule::Rule, AstNode};
#[derive(Debug, Error, Diagnostic)] #[derive(Debug, Error, Diagnostic)]
@ -163,8 +165,7 @@ impl Rule for AltText {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { return }; let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { return };
let JSXElementName::Identifier(iden) = &jsx_el.name else { return }; let Some(name) = &get_element_type(ctx, jsx_el) else { return };
let name = iden.name.as_str();
// <img> // <img>
if let Some(custom_tags) = &self.img { if let Some(custom_tags) = &self.img {
@ -356,7 +357,7 @@ fn input_type_image_rule<'a>(node: &'a JSXOpeningElement<'a>, ctx: &LintContext<
fn test() { fn test() {
use crate::tester::Tester; use crate::tester::Tester;
fn array() -> serde_json::Value { fn config() -> serde_json::Value {
serde_json::json!([{ serde_json::json!([{
"img": ["Thumbnail", "Image"], "img": ["Thumbnail", "Image"],
"object": ["Object"], "object": ["Object"],
@ -366,177 +367,179 @@ fn test() {
} }
let pass = vec![ let pass = vec![
(r#"<img alt="foo" />;"#, None), (r#"<img alt="foo" />;"#, None, None),
(r#"<img alt={"foo"} />;"#, None), (r#"<img alt={"foo"} />;"#, None, None),
(r"<img alt={alt} />;", None), (r"<img alt={alt} />;", None, None),
(r#"<img ALT="foo" />;"#, None), (r#"<img ALT="foo" />;"#, None, None),
(r"<img ALT={`This is the ${alt} text`} />;", None), (r"<img ALT={`This is the ${alt} text`} />;", None, None),
(r#"<img ALt="foo" />;"#, None), (r#"<img ALt="foo" />;"#, None, None),
(r#"<img alt="foo" salt={undefined} />;"#, None), (r#"<img alt="foo" salt={undefined} />;"#, None, None),
(r#"<img {...this.props} alt="foo" />"#, None), (r#"<img {...this.props} alt="foo" />"#, None, None),
(r"<a />", None), (r"<a />", None, None),
(r"<div />", None), (r"<div />", None, None),
(r"<img alt={function(e) {} } />", None), (r"<img alt={function(e) {} } />", None, None),
(r"<div alt={function(e) {} } />", None), (r"<div alt={function(e) {} } />", None, None),
(r"<img alt={() => void 0} />", None), (r"<img alt={() => void 0} />", None, None),
(r"<IMG />", None), (r"<IMG />", None, None),
(r"<UX.Layout>test</UX.Layout>", None), (r"<UX.Layout>test</UX.Layout>", None, None),
(r#"<img alt={alt || "Alt text" } />"#, None), (r#"<img alt={alt || "Alt text" } />"#, None, None),
(r"<img alt={photo.caption} />;", None), (r"<img alt={photo.caption} />;", None, None),
(r"<img alt={bar()} />;", None), (r"<img alt={bar()} />;", None, None),
(r#"<img alt={foo.bar || ""} />"#, None), (r#"<img alt={foo.bar || ""} />"#, None, None),
(r#"<img alt={bar() || ""} />"#, None), (r#"<img alt={bar() || ""} />"#, None, None),
(r#"<img alt={foo.bar() || ""} />"#, None), (r#"<img alt={foo.bar() || ""} />"#, None, None),
(r#"<img alt="" />"#, None), (r#"<img alt="" />"#, None, None),
(r"<img alt={`${undefined}`} />", None), (r"<img alt={`${undefined}`} />", None, None),
(r#"<img alt=" " />"#, None), (r#"<img alt=" " />"#, None, None),
(r#"<img alt="" role="presentation" />"#, None), (r#"<img alt="" role="presentation" />"#, None, None),
(r#"<img alt="" role="none" />"#, None), (r#"<img alt="" role="none" />"#, None, None),
(r#"<img alt="" role={`presentation`} />"#, None), (r#"<img alt="" role={`presentation`} />"#, None, None),
(r#"<img alt="" role={"presentation"} />"#, None), (r#"<img alt="" role={"presentation"} />"#, None, None),
(r#"<img alt="this is lit..." role="presentation" />"#, None), (r#"<img alt="this is lit..." role="presentation" />"#, None, None),
(r#"<img alt={error ? "not working": "working"} />"#, None), (r#"<img alt={error ? "not working": "working"} />"#, None, None),
(r#"<img alt={undefined ? "working": "not working"} />"#, None), (r#"<img alt={undefined ? "working": "not working"} />"#, None, None),
(r#"<img alt={plugin.name + " Logo"} />"#, None), (r#"<img alt={plugin.name + " Logo"} />"#, None, None),
(r#"<img aria-label="foo" />"#, None), (r#"<img aria-label="foo" />"#, None, None),
(r#"<img aria-labelledby="id1" />"#, None), (r#"<img aria-labelledby="id1" />"#, None, None),
(r#"<object aria-label="foo" />"#, None), (r#"<object aria-label="foo" />"#, None, None),
(r#"<object aria-labelledby="id1" />"#, None), (r#"<object aria-labelledby="id1" />"#, None, None),
(r"<object>Foo</object>", None), (r"<object>Foo</object>", None, None),
(r"<object><p>This is descriptive!</p></object>", None), (r"<object><p>This is descriptive!</p></object>", None, None),
(r"<Object />", None), (r"<Object />", None, None),
(r#"<object title="An object" />"#, None), (r#"<object title="An object" />"#, None, None),
(r#"<area aria-label="foo" />"#, None), (r#"<area aria-label="foo" />"#, None, None),
(r#"<area aria-labelledby="id1" />"#, None), (r#"<area aria-labelledby="id1" />"#, None, None),
(r#"<area alt="" />"#, None), (r#"<area alt="" />"#, None, None),
(r#"<area alt="This is descriptive!" />"#, None), (r#"<area alt="This is descriptive!" />"#, None, None),
(r"<area alt={altText} />", None), (r"<area alt={altText} />", None, None),
(r"<Area />", None), (r"<Area />", None, None),
(r"<input />", None), (r"<input />", None, None),
(r#"<input type="foo" />"#, None), (r#"<input type="foo" />"#, None, None),
(r#"<input type="image" aria-label="foo" />"#, None), (r#"<input type="image" aria-label="foo" />"#, None, None),
(r#"<input type="image" aria-labelledby="id1" />"#, None), (r#"<input type="image" aria-labelledby="id1" />"#, None, None),
(r#"<input type="image" alt="" />"#, None), (r#"<input type="image" alt="" />"#, None, None),
(r#"<input type="image" alt="This is descriptive!" />"#, None), (r#"<input type="image" alt="This is descriptive!" />"#, None, None),
(r#"<input type="image" alt={altText} />"#, None), (r#"<input type="image" alt={altText} />"#, None, None),
(r"<InputImage />", None), (r"<InputImage />", None, None),
(r#"<Input type="image" alt="" />"#, None), (r#"<Input type="image" alt="" />"#, None, None),
// TODO: When polymorphic components are supported (r#"<SomeComponent as="input" type="image" alt="" />"#, None, None),
// (r#"<SomeComponent as="input" type="image" alt="" />"#, None), (r#"<Thumbnail alt="foo" />;"#, Some(config()), None),
(r#"<Thumbnail alt="foo" />;"#, Some(array())), (r#"<Thumbnail alt={"foo"} />;"#, Some(config()), None),
(r#"<Thumbnail alt={"foo"} />;"#, Some(array())), (r"<Thumbnail alt={alt} />;", Some(config()), None),
(r"<Thumbnail alt={alt} />;", Some(array())), (r#"<Thumbnail ALT="foo" />;"#, Some(config()), None),
(r#"<Thumbnail ALT="foo" />;"#, Some(array())), (r"<Thumbnail ALT={`This is the ${alt} text`} />;", Some(config()), None),
(r"<Thumbnail ALT={`This is the ${alt} text`} />;", Some(array())), (r#"<Thumbnail ALt="foo" />;"#, Some(config()), None),
(r#"<Thumbnail ALt="foo" />;"#, Some(array())), (r#"<Thumbnail alt="foo" salt={undefined} />;"#, Some(config()), None),
(r#"<Thumbnail alt="foo" salt={undefined} />;"#, Some(array())), (r#"<Thumbnail {...this.props} alt="foo" />"#, Some(config()), None),
(r#"<Thumbnail {...this.props} alt="foo" />"#, Some(array())), (r"<thumbnail />", Some(config()), None),
(r"<thumbnail />", Some(array())), (r"<Thumbnail alt={function(e) {} } />", Some(config()), None),
(r"<Thumbnail alt={function(e) {} } />", Some(array())), (r"<div alt={function(e) {} } />", Some(config()), None),
(r"<div alt={function(e) {} } />", Some(array())), (r"<Thumbnail alt={() => void 0} />", Some(config()), None),
(r"<Thumbnail alt={() => void 0} />", Some(array())), (r"<THUMBNAIL />", Some(config()), None),
(r"<THUMBNAIL />", Some(array())), (r#"<Thumbnail alt={alt || "foo" } />"#, Some(config()), None),
(r#"<Thumbnail alt={alt || "foo" } />"#, Some(array())), (r#"<Image alt="foo" />;"#, Some(config()), None),
(r#"<Image alt="foo" />;"#, Some(array())), (r#"<Image alt={"foo"} />;"#, Some(config()), None),
(r#"<Image alt={"foo"} />;"#, Some(array())), (r"<Image alt={alt} />;", Some(config()), None),
(r"<Image alt={alt} />;", Some(array())), (r#"<Image ALT="foo" />;"#, Some(config()), None),
(r#"<Image ALT="foo" />;"#, Some(array())), (r"<Image ALT={`This is the ${alt} text`} />;", Some(config()), None),
(r"<Image ALT={`This is the ${alt} text`} />;", Some(array())), (r#"<Image ALt="foo" />;"#, Some(config()), None),
(r#"<Image ALt="foo" />;"#, Some(array())), (r#"<Image alt="foo" salt={undefined} />;"#, Some(config()), None),
(r#"<Image alt="foo" salt={undefined} />;"#, Some(array())), (r#"<Image {...this.props} alt="foo" />"#, Some(config()), None),
(r#"<Image {...this.props} alt="foo" />"#, Some(array())), (r"<image />", Some(config()), None),
(r"<image />", Some(array())), (r"<Image alt={function(e) {} } />", Some(config()), None),
(r"<Image alt={function(e) {} } />", Some(array())), (r"<div alt={function(e) {} } />", Some(config()), None),
(r"<div alt={function(e) {} } />", Some(array())), (r"<Image alt={() => void 0} />", Some(config()), None),
(r"<Image alt={() => void 0} />", Some(array())), (r"<IMAGE />", Some(config()), None),
(r"<IMAGE />", Some(array())), (r#"<Image alt={alt || "foo" } />"#, Some(config()), None),
(r#"<Image alt={alt || "foo" } />"#, Some(array())), (r#"<Object aria-label="foo" />"#, Some(config()), None),
(r#"<Object aria-label="foo" />"#, Some(array())), (r#"<Object aria-labelledby="id1" />"#, Some(config()), None),
(r#"<Object aria-labelledby="id1" />"#, Some(array())), (r"<Object>Foo</Object>", Some(config()), None),
(r"<Object>Foo</Object>", Some(array())), (r"<Object><p>This is descriptive!</p></Object>", Some(config()), None),
(r"<Object><p>This is descriptive!</p></Object>", Some(array())), (r#"<Object title="An object" />"#, Some(config()), None),
(r#"<Object title="An object" />"#, Some(array())), (r#"<Area aria-label="foo" />"#, Some(config()), None),
(r#"<Area aria-label="foo" />"#, Some(array())), (r#"<Area aria-labelledby="id1" />"#, Some(config()), None),
(r#"<Area aria-labelledby="id1" />"#, Some(array())), (r#"<Area alt="" />"#, Some(config()), None),
(r#"<Area alt="" />"#, Some(array())), (r#"<Area alt="This is descriptive!" />"#, Some(config()), None),
(r#"<Area alt="This is descriptive!" />"#, Some(array())), (r"<Area alt={altText} />", Some(config()), None),
(r"<Area alt={altText} />", Some(array())), (r#"<InputImage aria-label="foo" />"#, Some(config()), None),
(r#"<InputImage aria-label="foo" />"#, Some(array())), (r#"<InputImage aria-labelledby="id1" />"#, Some(config()), None),
(r#"<InputImage aria-labelledby="id1" />"#, Some(array())), (r#"<InputImage alt="" />"#, Some(config()), None),
(r#"<InputImage alt="" />"#, Some(array())), (r#"<InputImage alt="This is descriptive!" />"#, Some(config()), None),
(r#"<InputImage alt="This is descriptive!" />"#, Some(array())), (r"<InputImage alt={altText} />", Some(config()), None),
(r"<InputImage alt={altText} />", Some(array())),
]; ];
let fail = vec![ let fail = vec![
(r"<img />;", None), (r"<img />;", None, None),
(r"<img alt />;", None), (r"<img alt />;", None, None),
(r"<img alt={undefined} />;", None), (r"<img alt={undefined} />;", None, None),
(r#"<img src="xyz" />"#, None), (r#"<img src="xyz" />"#, None, None),
(r"<img role />", None), (r"<img role />", None, None),
(r"<img {...this.props} />", None), (r"<img {...this.props} />", None, None),
// TODO: Could support if get_prop_value could evaluate // TODO: Could support if get_prop_value could evaluate
// some logical expressions // some logical expressions
// (r#"<img alt={false || false} />"#, None), // (r#"<img alt={false || false} />"#, None, None),
(r#"<img alt={undefined} role="presentation" />;"#, None), (r#"<img alt={undefined} role="presentation" />;"#, None, None),
(r#"<img alt role="presentation" />;"#, None), (r#"<img alt role="presentation" />;"#, None, None),
(r#"<img role="presentation" />;"#, None), (r#"<img role="presentation" />;"#, None, None),
(r#"<img role="none" />;"#, None), (r#"<img role="none" />;"#, None, None),
(r"<img aria-label={undefined} />", None), (r"<img aria-label={undefined} />", None, None),
(r"<img aria-labelledby={undefined} />", None), (r"<img aria-labelledby={undefined} />", None, None),
(r#"<img aria-label="" />"#, None), (r#"<img aria-label="" />"#, None, None),
(r#"<img aria-labelledby="" />"#, None), (r#"<img aria-labelledby="" />"#, None, None),
// TODO: When polymorphic components are supported (
// (r#"<SomeComponent as="img" aria-label="" />"#, None), r#"<SomeComponent as="img" aria-label="" />"#,
(r"<object />", None), None,
(r"<object><div aria-hidden /></object>", None), Some(serde_json::json!({ "jsx-a11y": { "polymorphicPropName": "as" } })),
(r"<object title={undefined} />", None), ),
(r#"<object aria-label="" />"#, None), (r"<object />", None, None),
(r#"<object aria-labelledby="" />"#, None), (r"<object><div aria-hidden /></object>", None, None),
(r"<object aria-label={undefined} />", None), (r"<object title={undefined} />", None, None),
(r"<object aria-labelledby={undefined} />", None), (r#"<object aria-label="" />"#, None, None),
(r"<area />", None), (r#"<object aria-labelledby="" />"#, None, None),
(r"<area alt />", None), (r"<object aria-label={undefined} />", None, None),
(r"<area alt={undefined} />", None), (r"<object aria-labelledby={undefined} />", None, None),
(r#"<area src="xyz" />"#, None), (r"<area />", None, None),
(r"<area {...this.props} />", None), (r"<area alt />", None, None),
(r#"<area aria-label="" />"#, None), (r"<area alt={undefined} />", None, None),
(r"<area aria-label={undefined} />", None), (r#"<area src="xyz" />"#, None, None),
(r#"<area aria-labelledby="" />"#, None), (r"<area {...this.props} />", None, None),
(r"<area aria-labelledby={undefined} />", None), (r#"<area aria-label="" />"#, None, None),
(r#"<input type="image" />"#, None), (r"<area aria-label={undefined} />", None, None),
(r#"<input type="image" alt />"#, None), (r#"<area aria-labelledby="" />"#, None, None),
(r#"<input type="image" alt={undefined} />"#, None), (r"<area aria-labelledby={undefined} />", None, None),
(r#"<input type="image">Foo</input>"#, None), (r#"<input type="image" />"#, None, None),
(r#"<input type="image" {...this.props} />"#, None), (r#"<input type="image" alt />"#, None, None),
(r#"<input type="image" aria-label="" />"#, None), (r#"<input type="image" alt={undefined} />"#, None, None),
(r#"<input type="image" aria-label={undefined} />"#, None), (r#"<input type="image">Foo</input>"#, None, None),
(r#"<input type="image" aria-labelledby="" />"#, None), (r#"<input type="image" {...this.props} />"#, None, None),
(r#"<input type="image" aria-labelledby={undefined} />"#, None), (r#"<input type="image" aria-label="" />"#, None, None),
(r"<Thumbnail />;", Some(array())), (r#"<input type="image" aria-label={undefined} />"#, None, None),
(r"<Thumbnail alt />;", Some(array())), (r#"<input type="image" aria-labelledby="" />"#, None, None),
(r"<Thumbnail alt={undefined} />;", Some(array())), (r#"<input type="image" aria-labelledby={undefined} />"#, None, None),
(r#"<Thumbnail src="xyz" />"#, Some(array())), (r"<Thumbnail />;", Some(config()), None),
(r"<Thumbnail {...this.props} />", Some(array())), (r"<Thumbnail alt />;", Some(config()), None),
(r"<Image />;", Some(array())), (r"<Thumbnail alt={undefined} />;", Some(config()), None),
(r"<Image alt />;", Some(array())), (r#"<Thumbnail src="xyz" />"#, Some(config()), None),
(r"<Image alt={undefined} />;", Some(array())), (r"<Thumbnail {...this.props} />", Some(config()), None),
(r#"<Image src="xyz" />"#, Some(array())), (r"<Image />;", Some(config()), None),
(r"<Image {...this.props} />", Some(array())), (r"<Image alt />;", Some(config()), None),
(r"<Object />", Some(array())), (r"<Image alt={undefined} />;", Some(config()), None),
(r"<Object><div aria-hidden /></Object>", Some(array())), (r#"<Image src="xyz" />"#, Some(config()), None),
(r"<Object title={undefined} />", Some(array())), (r"<Image {...this.props} />", Some(config()), None),
(r"<Area />", Some(array())), (r"<Object />", Some(config()), None),
(r"<Area alt />", Some(array())), (r"<Object><div aria-hidden /></Object>", Some(config()), None),
(r"<Area alt={undefined} />", Some(array())), (r"<Object title={undefined} />", Some(config()), None),
(r#"<Area src="xyz" />"#, Some(array())), (r"<Area />", Some(config()), None),
(r"<Area {...this.props} />", Some(array())), (r"<Area alt />", Some(config()), None),
(r"<InputImage />", Some(array())), (r"<Area alt={undefined} />", Some(config()), None),
(r"<InputImage alt />", Some(array())), (r#"<Area src="xyz" />"#, Some(config()), None),
(r"<InputImage alt={undefined} />", Some(array())), (r"<Area {...this.props} />", Some(config()), None),
(r"<InputImage>Foo</InputImage>", Some(array())), (r"<InputImage />", Some(config()), None),
(r"<InputImage {...this.props} />", Some(array())), (r"<InputImage alt />", Some(config()), None),
(r#"<Input type="image" />"#, None), (r"<InputImage alt={undefined} />", Some(config()), None),
(r"<InputImage>Foo</InputImage>", Some(config()), None),
(r"<InputImage {...this.props} />", Some(config()), None),
(r#"<Input type="image" />"#, None, None),
]; ];
Tester::new(AltText::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot(); Tester::new(AltText::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot();

View file

@ -17,7 +17,7 @@ use oxc_allocator::Vec;
use crate::{ use crate::{
context::LintContext, context::LintContext,
rule::Rule, rule::Rule,
utils::{get_prop_value, has_jsx_prop_lowercase}, utils::{get_element_type, get_prop_value, has_jsx_prop_lowercase},
AstNode, AstNode,
}; };
@ -75,8 +75,7 @@ declare_oxc_lint!(
impl Rule for AnchorHasContent { impl Rule for AnchorHasContent {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXElement(jsx_el) = node.kind() { if let AstKind::JSXElement(jsx_el) = node.kind() {
let JSXElementName::Identifier(iden) = &jsx_el.opening_element.name else { return }; let Some(name) = &get_element_type(ctx, &jsx_el.opening_element) else { return };
let name = iden.name.as_str();
if name == "a" { if name == "a" {
// check self attr // check self attr
if has_jsx_prop_lowercase(&jsx_el.opening_element, "aria-hidden").is_some() { if has_jsx_prop_lowercase(&jsx_el.opening_element, "aria-hidden").is_some() {
@ -163,30 +162,36 @@ fn test() {
// https://raw.githubusercontent.com/jsx-eslint/eslint-plugin-jsx-a11y/main/__tests__/src/rules/anchor-has-content-test.js // https://raw.githubusercontent.com/jsx-eslint/eslint-plugin-jsx-a11y/main/__tests__/src/rules/anchor-has-content-test.js
let pass = vec![ let pass = vec![
(r"<div />;", None), (r"<div />;", None, None),
(r"<a>Foo</a>", None), (r"<a>Foo</a>", None, None),
(r"<a><Bar /></a>", None), (r"<a><Bar /></a>", None, None),
(r"<a>{foo}</a>", None), (r"<a>{foo}</a>", None, None),
(r"<a>{foo.bar}</a>", None), (r"<a>{foo.bar}</a>", None, None),
(r#"<a dangerouslySetInnerHTML={{ __html: "foo" }} />"#, None), (r#"<a dangerouslySetInnerHTML={{ __html: "foo" }} />"#, None, None),
(r"<a children={children} />", None), (r"<a children={children} />", None, None),
// TODO: (
// { code: '<Link>foo</Link>', settings: { 'jsx-a11y': { components: { Link: 'a' } } }, }, r"<Link>foo</Link>",
(r"<a title={title} />", None), None,
(r"<a aria-label={ariaLabel} />", None), Some(serde_json::json!({ "jsx-a11y": { "components": { "Link": "a" } } })),
(r"<a title={title} aria-label={ariaLabel} />", None), ),
(r"<a><Bar aria-hidden />Foo</a>", None), (r"<a title={title} />", None, None),
(r"<a aria-label={ariaLabel} />", None, None),
(r"<a title={title} aria-label={ariaLabel} />", None, None),
(r"<a><Bar aria-hidden />Foo</a>", None, None),
]; ];
let fail = vec![ let fail = vec![
(r"<a />", None), (r"<a />", None, None),
(r"<a><Bar aria-hidden /></a>", None), (r"<a><Bar aria-hidden /></a>", None, None),
(r"<a>{undefined}</a>", None), (r"<a>{undefined}</a>", None, None),
// TODO: (
// { code: '<Link />', errors: [expectedError], settings: { 'jsx-a11y': { components: { Link: 'a' } } }, }, r"<Link />",
(r"<a aria-hidden ></a>", None), None,
(r"<a>{null}</a>", None), Some(serde_json::json!({ "jsx-a11y": { "components": { "Link": "a" } } })),
(r"<a title />", None), ),
(r"<a aria-hidden ></a>", None, None),
(r"<a>{null}</a>", None, None),
(r"<a title />", None, None),
]; ];
Tester::new(AnchorHasContent::NAME, pass, fail).test_and_snapshot(); Tester::new(AnchorHasContent::NAME, pass, fail).test_and_snapshot();

View file

@ -9,7 +9,12 @@ use oxc_diagnostics::{
use oxc_macros::declare_oxc_lint; use oxc_macros::declare_oxc_lint;
use oxc_span::Span; use oxc_span::Span;
use crate::{context::LintContext, rule::Rule, utils::has_jsx_prop_lowercase, AstNode}; use crate::{
context::LintContext,
rule::Rule,
utils::{get_element_type, has_jsx_prop_lowercase},
AstNode,
};
#[derive(Debug, Error, Diagnostic)] #[derive(Debug, Error, Diagnostic)]
enum AnchorIsValidDiagnostic { enum AnchorIsValidDiagnostic {
@ -108,10 +113,12 @@ declare_oxc_lint!(
); );
impl Rule for AnchorIsValid { impl Rule for AnchorIsValid {
// TODO: Implement from_configuration() and test it
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXElement(jsx_el) = node.kind() { if let AstKind::JSXElement(jsx_el) = node.kind() {
let JSXElementName::Identifier(ident) = &jsx_el.opening_element.name else { return }; let JSXElementName::Identifier(ident) = &jsx_el.opening_element.name else { return };
let name = ident.name.as_str(); let Some(name) = &get_element_type(ctx, &jsx_el.opening_element) else { return };
if name == "a" { if name == "a" {
if let Option::Some(herf_attr) = if let Option::Some(herf_attr) =
has_jsx_prop_lowercase(&jsx_el.opening_element, "href") has_jsx_prop_lowercase(&jsx_el.opening_element, "href")
@ -212,24 +219,24 @@ fn test() {
// https://raw.githubusercontent.com/jsx-eslint/eslint-plugin-jsx-a11y/main/__tests__/src/rules/anchor-is-valid-test.js // https://raw.githubusercontent.com/jsx-eslint/eslint-plugin-jsx-a11y/main/__tests__/src/rules/anchor-is-valid-test.js
let pass = vec![ let pass = vec![
(r"<Anchor />", None), (r"<Anchor />", None, None),
(r"<a {...props} />", None), (r"<a {...props} />", None, None),
(r"<a href='foo' />", None), (r"<a href='foo' />", None, None),
(r"<a href={foo} />", None), (r"<a href={foo} />", None, None),
(r"<a href='/foo' />", None), (r"<a href='/foo' />", None, None),
(r"<a href='https://foo.bar.com' />", None), (r"<a href='https://foo.bar.com' />", None, None),
(r"<div href='foo' />", None), (r"<div href='foo' />", None, None),
(r"<a href='javascript' />", None), (r"<a href='javascript' />", None, None),
(r"<a href='javascriptFoo' />", None), (r"<a href='javascriptFoo' />", None, None),
(r"<a href={`#foo`}/>", None), (r"<a href={`#foo`}/>", None, None),
(r"<a href={'foo'}/>", None), (r"<a href={'foo'}/>", None, None),
(r"<a href={'javascript'}/>", None), (r"<a href={'javascript'}/>", None, None),
(r"<a href={`#javascript`}/>", None), (r"<a href={`#javascript`}/>", None, None),
(r"<a href='#foo' />", None), (r"<a href='#foo' />", None, None),
(r"<a href='#javascript' />", None), (r"<a href='#javascript' />", None, None),
(r"<a href='#javascriptFoo' />", None), (r"<a href='#javascriptFoo' />", None, None),
(r"<UX.Layout>test</UX.Layout>", None), (r"<UX.Layout>test</UX.Layout>", None, None),
(r"<a href={this} />", None), (r"<a href={this} />", None, None),
// (r#"<Anchor {...props} />"#, Some(serde_json::json!(components))), // (r#"<Anchor {...props} />"#, Some(serde_json::json!(components))),
// (r#"<Anchor href='foo' />"#, Some(serde_json::json!(components))), // (r#"<Anchor href='foo' />"#, Some(serde_json::json!(components))),
// (r#"<Anchor href={foo} />"#, Some(serde_json::json!(components))), // (r#"<Anchor href={foo} />"#, Some(serde_json::json!(components))),
@ -248,7 +255,13 @@ fn test() {
// (r#"<Link href={`#foo`}/>"#, Some(serde_json::json!(components))), // (r#"<Link href={`#foo`}/>"#, Some(serde_json::json!(components))),
// (r#"<Link href={'foo'}/>"#, Some(serde_json::json!(components))), // (r#"<Link href={'foo'}/>"#, Some(serde_json::json!(components))),
// (r#"<Link href='#foo' />"#, Some(serde_json::json!(components))), // (r#"<Link href='#foo' />"#, Some(serde_json::json!(components))),
(r"<Link href='#foo' />", None), (
r"<Link href='#foo' />",
None,
Some(
serde_json::json!({ "jsx-a11y": { "components": { "Anchor": "a", "Link": "a" } } }),
),
),
// (r#"<a {...props} />"#, Some(serde_json::json!(specialLink))), // (r#"<a {...props} />"#, Some(serde_json::json!(specialLink))),
// (r#"<a hrefLeft='foo' />"#, Some(serde_json::json!(specialLink))), // (r#"<a hrefLeft='foo' />"#, Some(serde_json::json!(specialLink))),
// (r#"<a hrefLeft={foo} />"#, Some(serde_json::json!(specialLink))), // (r#"<a hrefLeft={foo} />"#, Some(serde_json::json!(specialLink))),
@ -284,16 +297,16 @@ fn test() {
// (r#"<Anchor hrefLeft={'foo'}/>"#, Some(serde_json::json!(componentsAndSpecialLink))), // (r#"<Anchor hrefLeft={'foo'}/>"#, Some(serde_json::json!(componentsAndSpecialLink))),
// (r#"<Anchor hrefLeft='#foo' />"#, Some(serde_json::json!(componentsAndSpecialLink))), // (r#"<Anchor hrefLeft='#foo' />"#, Some(serde_json::json!(componentsAndSpecialLink))),
// (r#"<UX.Layout>test</UX.Layout>"#, Some(serde_json::json!(componentsAndSpecialLink))), // (r#"<UX.Layout>test</UX.Layout>"#, Some(serde_json::json!(componentsAndSpecialLink))),
(r"<a {...props} onClick={() => void 0} />", None), (r"<a {...props} onClick={() => void 0} />", None, None),
(r"<a href='foo' onClick={() => void 0} />", None), (r"<a href='foo' onClick={() => void 0} />", None, None),
(r"<a href={foo} onClick={() => void 0} />", None), (r"<a href={foo} onClick={() => void 0} />", None, None),
(r"<a href='/foo' onClick={() => void 0} />", None), (r"<a href='/foo' onClick={() => void 0} />", None, None),
(r"<a href='https://foo.bar.com' onClick={() => void 0} />", None), (r"<a href='https://foo.bar.com' onClick={() => void 0} />", None, None),
(r"<div href='foo' onClick={() => void 0} />", None), (r"<div href='foo' onClick={() => void 0} />", None, None),
(r"<a href={`#foo`} onClick={() => void 0} />", None), (r"<a href={`#foo`} onClick={() => void 0} />", None, None),
(r"<a href={'foo'} onClick={() => void 0} />", None), (r"<a href={'foo'} onClick={() => void 0} />", None, None),
(r"<a href='#foo' onClick={() => void 0} />", None), (r"<a href='#foo' onClick={() => void 0} />", None, None),
(r"<a href={this} onClick={() => void 0} />", None), (r"<a href={this} onClick={() => void 0} />", None, None),
// (r#"<Anchor {...props} onClick={() => void 0} />"#, Some(serde_json::json!(components))), // (r#"<Anchor {...props} onClick={() => void 0} />"#, Some(serde_json::json!(components))),
// (r#"<Anchor href='foo' onClick={() => void 0} />"#, Some(serde_json::json!(components))), // (r#"<Anchor href='foo' onClick={() => void 0} />"#, Some(serde_json::json!(components))),
// (r#"<Anchor href={foo} onClick={() => void 0} />"#, Some(serde_json::json!(components))), // (r#"<Anchor href={foo} onClick={() => void 0} />"#, Some(serde_json::json!(components))),
@ -460,18 +473,18 @@ fn test() {
]; ];
let fail = vec![ let fail = vec![
(r"<a />", None), (r"<a />", None, None),
(r"<a href={undefined} />", None), (r"<a href={undefined} />", None, None),
(r"<a href={null} />", None), (r"<a href={null} />", None, None),
(r"<a href=' />;", None), (r"<a href=' />;", None, None),
(r"<a href='#' />", None), (r"<a href='#' />", None, None),
(r"<a href={'#'} />", None), (r"<a href={'#'} />", None, None),
(r"<a href='javascript:void(0)' />", None), (r"<a href='javascript:void(0)' />", None, None),
(r"<a href={'javascript:void(0)'} />", None), (r"<a href={'javascript:void(0)'} />", None, None),
(r"<a onClick={() => void 0} />", None), (r"<a onClick={() => void 0} />", None, None),
(r"<a href='#' onClick={() => void 0} />", None), (r"<a href='#' onClick={() => void 0} />", None, None),
(r"<a href='javascript:void(0)' onClick={() => void 0} />", None), (r"<a href='javascript:void(0)' onClick={() => void 0} />", None, None),
(r"<a href={'javascript:void(0)'} onClick={() => void 0} />", None), (r"<a href={'javascript:void(0)'} onClick={() => void 0} />", None, None),
// (r#"<Link />"#, Some(serde_json::json!(components))), // (r#"<Link />"#, Some(serde_json::json!(components))),
// (r#"<Link href={undefined} />"#, Some(serde_json::json!(components))), // (r#"<Link href={undefined} />"#, Some(serde_json::json!(components))),
// (r#"<Link href={null} />"#, Some(serde_json::json!(components))), // (r#"<Link href={null} />"#, Some(serde_json::json!(components))),
@ -505,7 +518,13 @@ fn test() {
// r#"<Anchor href={'javascript:void(0)'} onClick={() => void 0} />"#, // r#"<Anchor href={'javascript:void(0)'} onClick={() => void 0} />"#,
// Some(serde_json::json!(components)), // Some(serde_json::json!(components)),
// ), // ),
// (r#"<Link href='#' onClick={() => void 0} />"#, None), (
r"<Link href='#' onClick={() => void 0} />",
None,
Some(
serde_json::json!({ "jsx-a11y": { "components": { "Anchor": "a", "Link": "a" } } }),
),
),
// (r#"<a hrefLeft={undefined} />"#, Some(serde_json::json!(specialLink))), // (r#"<a hrefLeft={undefined} />"#, Some(serde_json::json!(specialLink))),
// (r#"<a hrefLeft={null} />"#, Some(serde_json::json!(specialLink))), // (r#"<a hrefLeft={null} />"#, Some(serde_json::json!(specialLink))),
// (r#"<a hrefLeft=' />;"#, Some(serde_json::json!(specialLink))), // (r#"<a hrefLeft=' />;"#, Some(serde_json::json!(specialLink))),

View file

@ -1,4 +1,4 @@
use oxc_ast::{ast::JSXElementName, AstKind}; use oxc_ast::AstKind;
use oxc_diagnostics::{ use oxc_diagnostics::{
miette::{self, Diagnostic}, miette::{self, Diagnostic},
thiserror::Error, thiserror::Error,
@ -9,7 +9,7 @@ use oxc_span::Span;
use crate::{ use crate::{
context::LintContext, context::LintContext,
rule::Rule, rule::Rule,
utils::{is_hidden_from_screen_reader, object_has_accessible_child}, utils::{get_element_type, is_hidden_from_screen_reader, object_has_accessible_child},
AstNode, AstNode,
}; };
@ -87,12 +87,15 @@ impl Rule for HeadingHasContent {
return; return;
}; };
let JSXElementName::Identifier(iden) = &jsx_el.name else { // let JSXElementName::Identifier(iden) = &jsx_el.name else {
// return;
// };
// let name = iden.name.as_str();
let Some(name) = &get_element_type(ctx, jsx_el) else {
return; return;
}; };
let name = iden.name.as_str();
if !DEFAULT_COMPONENTS.iter().any(|&comp| comp == name) if !DEFAULT_COMPONENTS.iter().any(|&comp| comp == name)
&& !self && !self
.components .components
@ -127,50 +130,61 @@ fn test() {
}]) }])
} }
fn settings() -> serde_json::Value {
serde_json::json!({
"jsx-a11y": {
"components": {
"CustomInput": "input",
"Title": "h1",
"Heading": "h2",
},
},
})
}
let pass = vec![ let pass = vec![
// DEFAULT ELEMENT TESTS // DEFAULT ELEMENT TESTS
(r"<h1>Foo</h1>", None), (r"<h1>Foo</h1>", None, None),
(r"<h2>Foo</h2>", None), (r"<h2>Foo</h2>", None, None),
(r"<h3>Foo</h3>", None), (r"<h3>Foo</h3>", None, None),
(r"<h4>Foo</h4>", None), (r"<h4>Foo</h4>", None, None),
(r"<h5>Foo</h5>", None), (r"<h5>Foo</h5>", None, None),
(r"<h6>Foo</h6>", None), (r"<h6>Foo</h6>", None, None),
(r"<h6>123</h6>", None), (r"<h6>123</h6>", None, None),
(r"<h1><Bar /></h1>", None), (r"<h1><Bar /></h1>", None, None),
(r"<h1>{foo}</h1>", None), (r"<h1>{foo}</h1>", None, None),
(r"<h1>{foo.bar}</h1>", None), (r"<h1>{foo.bar}</h1>", None, None),
(r#"<h1 dangerouslySetInnerHTML={{ __html: "foo" }} />"#, None), (r#"<h1 dangerouslySetInnerHTML={{ __html: "foo" }} />"#, None, None),
(r"<h1 children={children} />", None), (r"<h1 children={children} />", None, None),
// CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION // CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
(r"<Heading>Foo</Heading>", Some(components())), (r"<Heading>Foo</Heading>", Some(components()), None),
(r"<Title>Foo</Title>", Some(components())), (r"<Title>Foo</Title>", Some(components()), None),
(r"<Heading><Bar /></Heading>", Some(components())), (r"<Heading><Bar /></Heading>", Some(components()), None),
(r"<Heading>{foo}</Heading>", Some(components())), (r"<Heading>{foo}</Heading>", Some(components()), None),
(r"<Heading>{foo.bar}</Heading>", Some(components())), (r"<Heading>{foo.bar}</Heading>", Some(components()), None),
(r#"<Heading dangerouslySetInnerHTML={{ __html: "foo" }} />"#, Some(components())), (r#"<Heading dangerouslySetInnerHTML={{ __html: "foo" }} />"#, Some(components()), None),
(r"<Heading children={children} />", Some(components())), (r"<Heading children={children} />", Some(components()), None),
(r"<h1 aria-hidden />", Some(components())), (r"<h1 aria-hidden />", Some(components()), None),
// TODO: When polymorphic components are supported
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS // CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
// (r"<Heading>Foo</Heading>", None), (r"<Heading>Foo</Heading>", None, Some(settings())),
// (r#"<h1><CustomInput type="hidden" /></h1>"#, None), (r#"<h1><CustomInput type="hidden" /></h1>"#, None, None),
]; ];
let fail = vec![ let fail = vec![
// DEFAULT ELEMENT TESTS // DEFAULT ELEMENT TESTS
(r"<h1 />", None), (r"<h1 />", None, None),
(r"<h1><Bar aria-hidden /></h1>", None), (r"<h1><Bar aria-hidden /></h1>", None, None),
(r"<h1>{undefined}</h1>", None), (r"<h1>{undefined}</h1>", None, None),
(r"<h1><></></h1>", None), (r"<h1><></></h1>", None, None),
(r#"<h1><input type="hidden" /></h1>"#, None), (r#"<h1><input type="hidden" /></h1>"#, None, None),
// CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION // CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
(r"<Heading />", Some(components())), (r"<Heading />", Some(components()), None),
(r"<Heading><Bar aria-hidden /></Heading>", Some(components())), (r"<Heading><Bar aria-hidden /></Heading>", Some(components()), None),
(r"<Heading>{undefined}</Heading>", Some(components())), (r"<Heading>{undefined}</Heading>", Some(components()), None),
// TODO: When polymorphic components are supported
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS // CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
// (r"<Heading />", None), (r"<Heading />", None, Some(settings())),
// (r#"<h1><CustomInput type="hidden" /></h1>"#, None), // TODO: This should be failed but pass for now
// (r#"<h1><CustomInput type="hidden" /></h1>"#, None, Some(settings())),
]; ];
Tester::new(HeadingHasContent::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot(); Tester::new(HeadingHasContent::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot();

View file

@ -12,7 +12,7 @@ use oxc_span::Span;
use crate::{ use crate::{
context::LintContext, context::LintContext,
rule::Rule, rule::Rule,
utils::{get_prop_value, has_jsx_prop_lowercase}, utils::{get_element_type, get_prop_value, has_jsx_prop_lowercase},
AstNode, AstNode,
}; };
@ -71,7 +71,7 @@ impl Rule for IframeHasTitle {
return; return;
}; };
let name = iden.name.as_str(); let Some(name) = get_element_type(ctx, jsx_el) else { return };
if name != "iframe" { if name != "iframe" {
return; return;
@ -127,29 +127,47 @@ fn test() {
let pass = vec![ let pass = vec![
// DEFAULT ELEMENT TESTS // DEFAULT ELEMENT TESTS
(r"<div />;", None), (r"<div />;", None, None),
(r"<iframe title='Unique title' />", None), (r"<iframe title='Unique title' />", None, None),
(r"<iframe title={foo} />", None), (r"<iframe title={foo} />", None, None),
(r"<FooComponent />", None), (r"<FooComponent />", None, None),
// TODO: When polymorphic components are supported
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS // CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
// (r"<FooComponent title='Unique title' />", None), (
r"<FooComponent title='Unique title' />",
None,
Some(serde_json::json!({
"jsx-a11y": {
"components": {
"FooComponent": "iframe",
},
},
})),
),
]; ];
let fail = vec![ let fail = vec![
// DEFAULT ELEMENT TESTS // DEFAULT ELEMENT TESTS
(r"<iframe />", None), (r"<iframe />", None, None),
(r"<iframe {...props} />", None), (r"<iframe {...props} />", None, None),
(r"<iframe title={undefined} />", None), (r"<iframe title={undefined} />", None, None),
(r"<iframe title='' />", None), (r"<iframe title='' />", None, None),
(r"<iframe title={false} />", None), (r"<iframe title={false} />", None, None),
(r"<iframe title={true} />", None), (r"<iframe title={true} />", None, None),
(r"<iframe title={''} />", None), (r"<iframe title={''} />", None, None),
(r"<iframe title={``} />", None), (r"<iframe title={``} />", None, None),
(r"<iframe title={42} />", None), (r"<iframe title={42} />", None, None),
// TODO: When polymorphic components are supported
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS // CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
// (r"<FooComponent />", None), (
r"<FooComponent />",
None,
Some(serde_json::json!({
"jsx-a11y": {
"components": {
"FooComponent": "iframe",
},
},
})),
),
]; ];
Tester::new(IframeHasTitle::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot(); Tester::new(IframeHasTitle::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot();

View file

@ -1,5 +1,5 @@
use oxc_ast::{ use oxc_ast::{
ast::{JSXAttributeItem, JSXAttributeValue, JSXElementName, JSXIdentifier, JSXOpeningElement}, ast::{JSXAttributeItem, JSXAttributeValue, JSXOpeningElement},
AstKind, AstKind,
}; };
use oxc_diagnostics::{ use oxc_diagnostics::{
@ -12,7 +12,7 @@ use oxc_span::Span;
use crate::{ use crate::{
context::LintContext, context::LintContext,
rule::Rule, rule::Rule,
utils::{has_jsx_prop_lowercase, parse_jsx_value}, utils::{get_element_type, has_jsx_prop_lowercase, parse_jsx_value},
AstNode, AstNode,
}; };
@ -47,7 +47,7 @@ impl Rule for NoAriaHiddenOnFocusable {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { return }; let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { return };
if let Some(aria_hidden_prop) = has_jsx_prop_lowercase(jsx_el, "aria-hidden") { if let Some(aria_hidden_prop) = has_jsx_prop_lowercase(jsx_el, "aria-hidden") {
if is_aria_hidden_true(aria_hidden_prop) && is_focusable(jsx_el) { if is_aria_hidden_true(aria_hidden_prop) && is_focusable(ctx, jsx_el) {
if let JSXAttributeItem::Attribute(boxed_attr) = aria_hidden_prop { if let JSXAttributeItem::Attribute(boxed_attr) = aria_hidden_prop {
ctx.diagnostic(NoAriaHiddenOnFocusableDiagnostic(boxed_attr.span)); ctx.diagnostic(NoAriaHiddenOnFocusableDiagnostic(boxed_attr.span));
} }
@ -84,10 +84,9 @@ fn is_aria_hidden_true(attr: &JSXAttributeItem) -> bool {
/// # Returns /// # Returns
/// ///
/// `true` if the element is focusable, `false` otherwise. /// `true` if the element is focusable, `false` otherwise.
fn is_focusable(element: &JSXOpeningElement) -> bool { fn is_focusable(ctx: &LintContext, element: &JSXOpeningElement) -> bool {
let tag_name = match &element.name { let Some(tag_name) = get_element_type(ctx, element) else {
JSXElementName::Identifier(JSXIdentifier { name, .. }) => name.as_str(), return false;
_ => return false,
}; };
if let Some(JSXAttributeItem::Attribute(attr)) = has_jsx_prop_lowercase(element, "tabIndex") { if let Some(JSXAttributeItem::Attribute(attr)) = has_jsx_prop_lowercase(element, "tabIndex") {
@ -96,7 +95,7 @@ fn is_focusable(element: &JSXOpeningElement) -> bool {
} }
} }
match tag_name { match tag_name.as_str() {
"a" | "area" => has_jsx_prop_lowercase(element, "href").is_some(), "a" | "area" => has_jsx_prop_lowercase(element, "href").is_some(),
"button" | "input" | "select" | "textarea" => { "button" | "input" | "select" | "textarea" => {
has_jsx_prop_lowercase(element, "disabled").is_none() has_jsx_prop_lowercase(element, "disabled").is_none()

View file

@ -1,7 +1,12 @@
use crate::{context::LintContext, rule::Rule, utils::has_jsx_prop_lowercase, AstNode}; use crate::{
context::LintContext,
rule::Rule,
utils::{get_element_type, has_jsx_prop_lowercase},
AstNode,
};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use oxc_ast::{ use oxc_ast::{
ast::{JSXAttributeItem, JSXAttributeValue, JSXElementName}, ast::{JSXAttributeItem, JSXAttributeValue},
AstKind, AstKind,
}; };
use oxc_diagnostics::{ use oxc_diagnostics::{
@ -52,7 +57,7 @@ impl PreferTagOverRole {
fn check_roles<'a>( fn check_roles<'a>(
role_prop: &JSXAttributeItem<'a>, role_prop: &JSXAttributeItem<'a>,
role_to_tag: &phf::Map<&str, &str>, role_to_tag: &phf::Map<&str, &str>,
jsx_name: &JSXElementName<'a>, jsx_name: &str,
ctx: &LintContext<'a>, ctx: &LintContext<'a>,
) { ) {
if let JSXAttributeItem::Attribute(attr) = role_prop { if let JSXAttributeItem::Attribute(attr) = role_prop {
@ -65,24 +70,21 @@ impl PreferTagOverRole {
} }
} }
fn check_role<'a>( fn check_role(
role: &str, role: &str,
role_to_tag: &phf::Map<&str, &str>, role_to_tag: &phf::Map<&str, &str>,
jsx_name: &JSXElementName<'a>, jsx_name: &str,
span: Span, span: Span,
ctx: &LintContext<'a>, ctx: &LintContext,
) { ) {
if let Some(tag) = role_to_tag.get(role) { if let Some(tag) = role_to_tag.get(role) {
match jsx_name { if jsx_name != *tag {
JSXElementName::Identifier(id) if id.name != *tag => {
ctx.diagnostic(PreferTagOverRoleDiagnostic { ctx.diagnostic(PreferTagOverRoleDiagnostic {
span, span,
tag: (*tag).to_string(), tag: (*tag).to_string(),
role: role.to_string(), role: role.to_string(),
}); });
} }
_ => {}
}
} }
} }
} }
@ -101,8 +103,10 @@ static ROLE_TO_TAG_MAP: Lazy<phf::Map<&'static str, &'static str>> = Lazy::new(|
impl Rule for PreferTagOverRole { impl Rule for PreferTagOverRole {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXOpeningElement(jsx_el) = node.kind() { if let AstKind::JSXOpeningElement(jsx_el) = node.kind() {
if let Some(name) = get_element_type(ctx, jsx_el) {
if let Some(role_prop) = has_jsx_prop_lowercase(jsx_el, "role") { if let Some(role_prop) = has_jsx_prop_lowercase(jsx_el, "role") {
Self::check_roles(role_prop, &ROLE_TO_TAG_MAP, &jsx_el.name, ctx); Self::check_roles(role_prop, &ROLE_TO_TAG_MAP, &name, ctx);
}
} }
} }
} }

View file

@ -101,6 +101,13 @@ expression: alt_text
╰──── ╰────
help: The alt attribute is preferred over aria-labelledby for images. help: The alt attribute is preferred over aria-labelledby for images.
⚠ eslint-plugin-jsx-a11y(alt-text): Missing value for aria-label attribute.
╭─[alt_text.tsx:1:1]
1 │ <SomeComponent as="img" aria-label="" />
· ────────────────────────────────────────
╰────
help: The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.
⚠ eslint-plugin-jsx-a11y(alt-text): Missing alternative text. ⚠ eslint-plugin-jsx-a11y(alt-text): Missing alternative text.
╭─[alt_text.tsx:1:1] ╭─[alt_text.tsx:1:1]
1 │ <object /> 1 │ <object />

View file

@ -24,6 +24,13 @@ expression: anchor_has_content
╰──── ╰────
help: Provide screen reader accessible content when using `a` elements. 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:2]
1 │ <Link />
· ────
╰────
help: Provide screen reader accessible content when using `a` elements.
⚠ eslint-plugin-jsx-a11y(anchor-has-content): Missing 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] ╭─[anchor_has_content.tsx:1:1]
1 │ <a aria-hidden ></a> 1 │ <a aria-hidden ></a>

View file

@ -86,3 +86,10 @@ expression: anchor_is_valid
╰──── ╰────
help: Use a `button` element instead of an `a` element. help: Use a `button` element instead of an `a` element.
⚠ eslint-plugin-jsx-a11y(anchor-is-valid): The a element has `href` and `onClick`.
╭─[anchor_is_valid.tsx:1:2]
1 │ <Link href='#' onClick={() => void 0} />
· ────
╰────
help: Use a `button` element instead of an `a` element.

View file

@ -59,3 +59,10 @@ expression: heading_has_content
╰──── ╰────
help: Provide screen reader accessible content when using heading elements. help: Provide screen reader accessible content when using heading elements.
⚠ eslint(heading-has-content): Headings must have content and the content must be accessible by a screen reader.
╭─[heading_has_content.tsx:1:1]
1 │ <Heading />
· ───────────
╰────
help: Provide screen reader accessible content when using heading elements.

View file

@ -66,3 +66,10 @@ expression: iframe_has_title
╰──── ╰────
help: Provide title property for iframe element. help: Provide title property for iframe element.
⚠ eslint-plugin-jsx-a11y(iframe-has-title): Missing `title` attribute for the `iframe` element.
╭─[iframe_has_title.tsx:1:2]
1 │ <FooComponent />
· ────────────
╰────
help: Provide title property for iframe element.

View file

@ -219,6 +219,7 @@ pub fn get_parent_es6_component<'a, 'b>(ctx: &'b LintContext<'a>) -> Option<&'b
}) })
} }
// ref: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/util/getElementType.js
pub fn get_element_type(context: &LintContext, element: &JSXOpeningElement) -> Option<String> { pub fn get_element_type(context: &LintContext, element: &JSXOpeningElement) -> Option<String> {
let JSXElementName::Identifier(ident) = &element.name else { let JSXElementName::Identifier(ident) = &element.name else {
return None; return None;
@ -226,19 +227,20 @@ pub fn get_element_type(context: &LintContext, element: &JSXOpeningElement) -> O
let ESLintSettings { jsx_a11y, .. } = context.settings(); let ESLintSettings { jsx_a11y, .. } = context.settings();
if let Some(polymorphic_prop_name_value) = &jsx_a11y.polymorphic_prop_name { let polymorphic_prop = jsx_a11y
if let Some(as_tag) = has_jsx_prop_lowercase(element, polymorphic_prop_name_value) { .polymorphic_prop_name
if let Some(JSXAttributeValue::StringLiteral(str)) = get_prop_value(as_tag) { .as_ref()
return Some(String::from(str.value.as_str())); .and_then(|polymorphic_prop_name_value| {
} has_jsx_prop_lowercase(element, polymorphic_prop_name_value)
} })
} .and_then(get_prop_value)
.and_then(|prop_value| match prop_value {
JSXAttributeValue::StringLiteral(str) => Some(str.value.as_str()),
_ => None,
});
let element_type = ident.name.as_str(); let raw_type = polymorphic_prop.unwrap_or_else(|| ident.name.as_str());
if let Some(val) = jsx_a11y.components.get(element_type) { Some(String::from(jsx_a11y.components.get(raw_type).map_or(raw_type, |c| c)))
return Some(String::from(val));
}
Some(String::from(element_type))
} }
pub fn parse_jsx_value(value: &JSXAttributeValue) -> Result<f64, ()> { pub fn parse_jsx_value(value: &JSXAttributeValue) -> Result<f64, ()> {