mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
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:
parent
2f6cf73d51
commit
2521b52011
13 changed files with 433 additions and 334 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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))),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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, ()> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue