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

View file

@ -17,7 +17,7 @@ use oxc_allocator::Vec;
use crate::{
context::LintContext,
rule::Rule,
utils::{get_prop_value, has_jsx_prop_lowercase},
utils::{get_element_type, get_prop_value, has_jsx_prop_lowercase},
AstNode,
};
@ -75,8 +75,7 @@ declare_oxc_lint!(
impl Rule for AnchorHasContent {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXElement(jsx_el) = node.kind() {
let JSXElementName::Identifier(iden) = &jsx_el.opening_element.name else { return };
let name = iden.name.as_str();
let Some(name) = &get_element_type(ctx, &jsx_el.opening_element) else { return };
if name == "a" {
// check self attr
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
let pass = vec![
(r"<div />;", None),
(r"<a>Foo</a>", None),
(r"<a><Bar /></a>", None),
(r"<a>{foo}</a>", None),
(r"<a>{foo.bar}</a>", None),
(r#"<a dangerouslySetInnerHTML={{ __html: "foo" }} />"#, None),
(r"<a children={children} />", None),
// TODO:
// { code: '<Link>foo</Link>', settings: { 'jsx-a11y': { components: { Link: 'a' } } }, },
(r"<a title={title} />", None),
(r"<a aria-label={ariaLabel} />", None),
(r"<a title={title} aria-label={ariaLabel} />", None),
(r"<a><Bar aria-hidden />Foo</a>", None),
(r"<div />;", None, None),
(r"<a>Foo</a>", None, None),
(r"<a><Bar /></a>", None, None),
(r"<a>{foo}</a>", None, None),
(r"<a>{foo.bar}</a>", None, None),
(r#"<a dangerouslySetInnerHTML={{ __html: "foo" }} />"#, None, None),
(r"<a children={children} />", None, None),
(
r"<Link>foo</Link>",
None,
Some(serde_json::json!({ "jsx-a11y": { "components": { "Link": "a" } } })),
),
(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![
(r"<a />", None),
(r"<a><Bar aria-hidden /></a>", None),
(r"<a>{undefined}</a>", None),
// TODO:
// { code: '<Link />', errors: [expectedError], settings: { 'jsx-a11y': { components: { Link: 'a' } } }, },
(r"<a aria-hidden ></a>", None),
(r"<a>{null}</a>", None),
(r"<a title />", None),
(r"<a />", None, None),
(r"<a><Bar aria-hidden /></a>", None, None),
(r"<a>{undefined}</a>", None, None),
(
r"<Link />",
None,
Some(serde_json::json!({ "jsx-a11y": { "components": { "Link": "a" } } })),
),
(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();

View file

@ -9,7 +9,12 @@ use oxc_diagnostics::{
use oxc_macros::declare_oxc_lint;
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)]
enum AnchorIsValidDiagnostic {
@ -108,10 +113,12 @@ declare_oxc_lint!(
);
impl Rule for AnchorIsValid {
// TODO: Implement from_configuration() and test it
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXElement(jsx_el) = node.kind() {
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 let Option::Some(herf_attr) =
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
let pass = vec![
(r"<Anchor />", None),
(r"<a {...props} />", None),
(r"<a href='foo' />", None),
(r"<a href={foo} />", None),
(r"<a href='/foo' />", None),
(r"<a href='https://foo.bar.com' />", None),
(r"<div href='foo' />", None),
(r"<a href='javascript' />", None),
(r"<a href='javascriptFoo' />", None),
(r"<a href={`#foo`}/>", None),
(r"<a href={'foo'}/>", None),
(r"<a href={'javascript'}/>", None),
(r"<a href={`#javascript`}/>", None),
(r"<a href='#foo' />", None),
(r"<a href='#javascript' />", None),
(r"<a href='#javascriptFoo' />", None),
(r"<UX.Layout>test</UX.Layout>", None),
(r"<a href={this} />", None),
(r"<Anchor />", None, None),
(r"<a {...props} />", None, None),
(r"<a href='foo' />", None, None),
(r"<a href={foo} />", None, None),
(r"<a href='/foo' />", None, None),
(r"<a href='https://foo.bar.com' />", None, None),
(r"<div href='foo' />", None, None),
(r"<a href='javascript' />", None, None),
(r"<a href='javascriptFoo' />", None, None),
(r"<a href={`#foo`}/>", None, None),
(r"<a href={'foo'}/>", None, None),
(r"<a href={'javascript'}/>", None, None),
(r"<a href={`#javascript`}/>", None, None),
(r"<a href='#foo' />", None, None),
(r"<a href='#javascript' />", None, None),
(r"<a href='#javascriptFoo' />", None, None),
(r"<UX.Layout>test</UX.Layout>", None, None),
(r"<a href={this} />", None, None),
// (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))),
@ -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' />", 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 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#"<UX.Layout>test</UX.Layout>"#, Some(serde_json::json!(componentsAndSpecialLink))),
(r"<a {...props} onClick={() => void 0} />", None),
(r"<a href='foo' onClick={() => void 0} />", None),
(r"<a href={foo} onClick={() => void 0} />", None),
(r"<a href='/foo' onClick={() => void 0} />", None),
(r"<a href='https://foo.bar.com' onClick={() => void 0} />", None),
(r"<div href='foo' onClick={() => void 0} />", None),
(r"<a href={`#foo`} onClick={() => void 0} />", None),
(r"<a href={'foo'} onClick={() => void 0} />", None),
(r"<a href='#foo' onClick={() => void 0} />", None),
(r"<a href={this} onClick={() => void 0} />", None),
(r"<a {...props} onClick={() => void 0} />", None, None),
(r"<a href='foo' onClick={() => void 0} />", None, None),
(r"<a href={foo} onClick={() => void 0} />", None, None),
(r"<a href='/foo' onClick={() => void 0} />", None, None),
(r"<a href='https://foo.bar.com' onClick={() => void 0} />", None, None),
(r"<div href='foo' onClick={() => void 0} />", None, None),
(r"<a href={`#foo`} onClick={() => void 0} />", None, None),
(r"<a href={'foo'} onClick={() => void 0} />", None, None),
(r"<a href='#foo' onClick={() => void 0} />", None, None),
(r"<a href={this} onClick={() => void 0} />", None, None),
// (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))),
@ -460,18 +473,18 @@ fn test() {
];
let fail = vec![
(r"<a />", None),
(r"<a href={undefined} />", None),
(r"<a href={null} />", None),
(r"<a href=' />;", None),
(r"<a href='#' />", None),
(r"<a href={'#'} />", None),
(r"<a href='javascript:void(0)' />", None),
(r"<a href={'javascript:void(0)'} />", None),
(r"<a onClick={() => void 0} />", None),
(r"<a href='#' onClick={() => void 0} />", None),
(r"<a href='javascript:void(0)' onClick={() => void 0} />", None),
(r"<a href={'javascript:void(0)'} onClick={() => void 0} />", None),
(r"<a />", None, None),
(r"<a href={undefined} />", None, None),
(r"<a href={null} />", None, None),
(r"<a href=' />;", None, None),
(r"<a href='#' />", None, None),
(r"<a href={'#'} />", None, None),
(r"<a href='javascript:void(0)' />", None, None),
(r"<a href={'javascript:void(0)'} />", None, None),
(r"<a onClick={() => void 0} />", None, None),
(r"<a href='#' onClick={() => void 0} />", None, None),
(r"<a href='javascript:void(0)' onClick={() => void 0} />", None, None),
(r"<a href={'javascript:void(0)'} onClick={() => void 0} />", None, None),
// (r#"<Link />"#, Some(serde_json::json!(components))),
// (r#"<Link href={undefined} />"#, 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} />"#,
// 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={null} />"#, 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::{
miette::{self, Diagnostic},
thiserror::Error,
@ -9,7 +9,7 @@ use oxc_span::Span;
use crate::{
context::LintContext,
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,
};
@ -87,12 +87,15 @@ impl Rule for HeadingHasContent {
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;
};
let name = iden.name.as_str();
if !DEFAULT_COMPONENTS.iter().any(|&comp| comp == name)
&& !self
.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![
// DEFAULT ELEMENT TESTS
(r"<h1>Foo</h1>", None),
(r"<h2>Foo</h2>", None),
(r"<h3>Foo</h3>", None),
(r"<h4>Foo</h4>", None),
(r"<h5>Foo</h5>", None),
(r"<h6>Foo</h6>", None),
(r"<h6>123</h6>", None),
(r"<h1><Bar /></h1>", None),
(r"<h1>{foo}</h1>", None),
(r"<h1>{foo.bar}</h1>", None),
(r#"<h1 dangerouslySetInnerHTML={{ __html: "foo" }} />"#, None),
(r"<h1 children={children} />", None),
(r"<h1>Foo</h1>", None, None),
(r"<h2>Foo</h2>", None, None),
(r"<h3>Foo</h3>", None, None),
(r"<h4>Foo</h4>", None, None),
(r"<h5>Foo</h5>", None, None),
(r"<h6>Foo</h6>", None, None),
(r"<h6>123</h6>", None, None),
(r"<h1><Bar /></h1>", None, None),
(r"<h1>{foo}</h1>", None, None),
(r"<h1>{foo.bar}</h1>", None, None),
(r#"<h1 dangerouslySetInnerHTML={{ __html: "foo" }} />"#, None, None),
(r"<h1 children={children} />", None, None),
// CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
(r"<Heading>Foo</Heading>", Some(components())),
(r"<Title>Foo</Title>", Some(components())),
(r"<Heading><Bar /></Heading>", Some(components())),
(r"<Heading>{foo}</Heading>", Some(components())),
(r"<Heading>{foo.bar}</Heading>", Some(components())),
(r#"<Heading dangerouslySetInnerHTML={{ __html: "foo" }} />"#, Some(components())),
(r"<Heading children={children} />", Some(components())),
(r"<h1 aria-hidden />", Some(components())),
// TODO: When polymorphic components are supported
(r"<Heading>Foo</Heading>", Some(components()), None),
(r"<Title>Foo</Title>", Some(components()), None),
(r"<Heading><Bar /></Heading>", Some(components()), None),
(r"<Heading>{foo}</Heading>", Some(components()), None),
(r"<Heading>{foo.bar}</Heading>", Some(components()), None),
(r#"<Heading dangerouslySetInnerHTML={{ __html: "foo" }} />"#, Some(components()), None),
(r"<Heading children={children} />", Some(components()), None),
(r"<h1 aria-hidden />", Some(components()), None),
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
// (r"<Heading>Foo</Heading>", None),
// (r#"<h1><CustomInput type="hidden" /></h1>"#, None),
(r"<Heading>Foo</Heading>", None, Some(settings())),
(r#"<h1><CustomInput type="hidden" /></h1>"#, None, None),
];
let fail = vec![
// DEFAULT ELEMENT TESTS
(r"<h1 />", None),
(r"<h1><Bar aria-hidden /></h1>", None),
(r"<h1>{undefined}</h1>", None),
(r"<h1><></></h1>", None),
(r#"<h1><input type="hidden" /></h1>"#, None),
(r"<h1 />", None, None),
(r"<h1><Bar aria-hidden /></h1>", None, None),
(r"<h1>{undefined}</h1>", None, None),
(r"<h1><></></h1>", None, None),
(r#"<h1><input type="hidden" /></h1>"#, None, None),
// CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
(r"<Heading />", Some(components())),
(r"<Heading><Bar aria-hidden /></Heading>", Some(components())),
(r"<Heading>{undefined}</Heading>", Some(components())),
// TODO: When polymorphic components are supported
(r"<Heading />", Some(components()), None),
(r"<Heading><Bar aria-hidden /></Heading>", Some(components()), None),
(r"<Heading>{undefined}</Heading>", Some(components()), None),
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
// (r"<Heading />", None),
// (r#"<h1><CustomInput type="hidden" /></h1>"#, None),
(r"<Heading />", None, Some(settings())),
// 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();

View file

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

View file

@ -1,5 +1,5 @@
use oxc_ast::{
ast::{JSXAttributeItem, JSXAttributeValue, JSXElementName, JSXIdentifier, JSXOpeningElement},
ast::{JSXAttributeItem, JSXAttributeValue, JSXOpeningElement},
AstKind,
};
use oxc_diagnostics::{
@ -12,7 +12,7 @@ use oxc_span::Span;
use crate::{
context::LintContext,
rule::Rule,
utils::{has_jsx_prop_lowercase, parse_jsx_value},
utils::{get_element_type, has_jsx_prop_lowercase, parse_jsx_value},
AstNode,
};
@ -47,7 +47,7 @@ impl Rule for NoAriaHiddenOnFocusable {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { return };
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 {
ctx.diagnostic(NoAriaHiddenOnFocusableDiagnostic(boxed_attr.span));
}
@ -84,10 +84,9 @@ fn is_aria_hidden_true(attr: &JSXAttributeItem) -> bool {
/// # Returns
///
/// `true` if the element is focusable, `false` otherwise.
fn is_focusable(element: &JSXOpeningElement) -> bool {
let tag_name = match &element.name {
JSXElementName::Identifier(JSXIdentifier { name, .. }) => name.as_str(),
_ => return false,
fn is_focusable(ctx: &LintContext, element: &JSXOpeningElement) -> bool {
let Some(tag_name) = get_element_type(ctx, element) else {
return false;
};
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(),
"button" | "input" | "select" | "textarea" => {
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 oxc_ast::{
ast::{JSXAttributeItem, JSXAttributeValue, JSXElementName},
ast::{JSXAttributeItem, JSXAttributeValue},
AstKind,
};
use oxc_diagnostics::{
@ -52,7 +57,7 @@ impl PreferTagOverRole {
fn check_roles<'a>(
role_prop: &JSXAttributeItem<'a>,
role_to_tag: &phf::Map<&str, &str>,
jsx_name: &JSXElementName<'a>,
jsx_name: &str,
ctx: &LintContext<'a>,
) {
if let JSXAttributeItem::Attribute(attr) = role_prop {
@ -65,23 +70,20 @@ impl PreferTagOverRole {
}
}
fn check_role<'a>(
fn check_role(
role: &str,
role_to_tag: &phf::Map<&str, &str>,
jsx_name: &JSXElementName<'a>,
jsx_name: &str,
span: Span,
ctx: &LintContext<'a>,
ctx: &LintContext,
) {
if let Some(tag) = role_to_tag.get(role) {
match jsx_name {
JSXElementName::Identifier(id) if id.name != *tag => {
ctx.diagnostic(PreferTagOverRoleDiagnostic {
span,
tag: (*tag).to_string(),
role: role.to_string(),
});
}
_ => {}
if jsx_name != *tag {
ctx.diagnostic(PreferTagOverRoleDiagnostic {
span,
tag: (*tag).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 {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXOpeningElement(jsx_el) = node.kind() {
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);
if let Some(name) = get_element_type(ctx, jsx_el) {
if let Some(role_prop) = has_jsx_prop_lowercase(jsx_el, "role") {
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.
⚠ 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.
╭─[alt_text.tsx:1:1]
1 │ <object />

View file

@ -24,6 +24,13 @@ expression: anchor_has_content
╰────
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.
╭─[anchor_has_content.tsx:1:1]
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.
⚠ 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.
⚠ 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.
⚠ 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> {
let JSXElementName::Identifier(ident) = &element.name else {
return None;
@ -226,19 +227,20 @@ pub fn get_element_type(context: &LintContext, element: &JSXOpeningElement) -> O
let ESLintSettings { jsx_a11y, .. } = context.settings();
if let Some(polymorphic_prop_name_value) = &jsx_a11y.polymorphic_prop_name {
if let Some(as_tag) = has_jsx_prop_lowercase(element, polymorphic_prop_name_value) {
if let Some(JSXAttributeValue::StringLiteral(str)) = get_prop_value(as_tag) {
return Some(String::from(str.value.as_str()));
}
}
}
let polymorphic_prop = jsx_a11y
.polymorphic_prop_name
.as_ref()
.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();
if let Some(val) = jsx_a11y.components.get(element_type) {
return Some(String::from(val));
}
Some(String::from(element_type))
let raw_type = polymorphic_prop.unwrap_or_else(|| ident.name.as_str());
Some(String::from(jsx_a11y.components.get(raw_type).map_or(raw_type, |c| c)))
}
pub fn parse_jsx_value(value: &JSXAttributeValue) -> Result<f64, ()> {