feat(linter): enhance get_element_type to resolve more element types (#7885)

I think it should if makes sense.

_Originally posted by @Boshen in
https://github.com/oxc-project/oxc/issues/7881#issuecomment-2543108246_
            

Since `jsx-a11y` supports these names, I have aligned them accordingly.
This commit is contained in:
dalaoshu 2024-12-14 22:45:37 +08:00 committed by GitHub
parent 7637aac21f
commit ee26b448cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 103 additions and 111 deletions

View file

@ -172,9 +172,8 @@ impl Rule for AltText {
let AstKind::JSXOpeningElement(jsx_el) = node.kind() else {
return;
};
let Some(name) = &get_element_type(ctx, jsx_el) else {
return;
};
let name = &get_element_type(ctx, jsx_el);
// <img>
if let Some(custom_tags) = &self.img {

View file

@ -107,9 +107,7 @@ impl Rule for AnchorAmbiguousText {
return;
};
let Some(name) = get_element_type(ctx, &jsx_el.opening_element) else {
return;
};
let name = get_element_type(ctx, &jsx_el.opening_element);
if name != "a" {
return;
@ -167,15 +165,14 @@ fn get_accessible_text<'a, 'b>(
};
}
if let Some(name) = get_element_type(ctx, &jsx_el.opening_element) {
if name == "img" {
if let Some(alt_text) = has_jsx_prop_ignore_case(&jsx_el.opening_element, "alt") {
if let Some(text) = get_string_literal_prop_value(alt_text) {
return Some(Cow::Borrowed(text));
};
let name = get_element_type(ctx, &jsx_el.opening_element);
if name == "img" {
if let Some(alt_text) = has_jsx_prop_ignore_case(&jsx_el.opening_element, "alt") {
if let Some(text) = get_string_literal_prop_value(alt_text) {
return Some(Cow::Borrowed(text));
};
}
};
};
}
if is_hidden_from_screen_reader(ctx, &jsx_el.opening_element) {
return None;

View file

@ -64,9 +64,8 @@ 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 Some(name) = &get_element_type(ctx, &jsx_el.opening_element) else {
return;
};
let name = get_element_type(ctx, &jsx_el.opening_element);
if name == "a" {
if is_hidden_from_screen_reader(ctx, &jsx_el.opening_element) {
return;

View file

@ -129,9 +129,8 @@ impl Rule for AnchorIsValid {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXElement(jsx_el) = node.kind() {
let Some(name) = &get_element_type(ctx, &jsx_el.opening_element) else {
return;
};
let name = get_element_type(ctx, &jsx_el.opening_element);
if name != "a" {
return;
};

View file

@ -64,9 +64,7 @@ impl Rule for AriaActivedescendantHasTabindex {
return;
};
let Some(element_type) = get_element_type(ctx, jsx_opening_el) else {
return;
};
let element_type = get_element_type(ctx, jsx_opening_el);
if !HTML_TAG.contains(&element_type) {
return;

View file

@ -143,9 +143,7 @@ impl Rule for AriaRole {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXElement(jsx_el) = node.kind() {
if let Some(aria_role) = has_jsx_prop(&jsx_el.opening_element, "role") {
let Some(element_type) = get_element_type(ctx, &jsx_el.opening_element) else {
return;
};
let element_type = get_element_type(ctx, &jsx_el.opening_element);
if self.ignore_non_dom && !HTML_TAG.contains(&element_type) {
return;

View file

@ -49,9 +49,7 @@ fn aria_unsupported_elements_diagnostic(span: Span, x1: &str) -> OxcDiagnostic {
impl Rule for AriaUnsupportedElements {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXOpeningElement(jsx_el) = node.kind() {
let Some(el_type) = get_element_type(ctx, jsx_el) else {
return;
};
let el_type = get_element_type(ctx, jsx_el);
if RESERVED_HTML_TAG.contains(&el_type) {
for attr in &jsx_el.attributes {
let attr = match attr {

View file

@ -181,9 +181,8 @@ impl Rule for AutocompleteValid {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXOpeningElement(jsx_el) = node.kind() {
let Some(name) = &get_element_type(ctx, jsx_el) else {
return;
};
let name = &get_element_type(ctx, jsx_el);
if !self.input_components.contains(name.as_ref()) {
return;
}

View file

@ -59,9 +59,8 @@ impl Rule for ClickEventsHaveKeyEvents {
};
// Check only native DOM elements or custom component via settings
let Some(element_type) = get_element_type(ctx, jsx_opening_el) else {
return;
};
let element_type = get_element_type(ctx, jsx_opening_el);
if !HTML_TAG.contains(&element_type) {
return;
};

View file

@ -89,9 +89,7 @@ impl Rule for HeadingHasContent {
// };
// let name = iden.name.as_str();
let Some(name) = &get_element_type(ctx, jsx_el) else {
return;
};
let name = &get_element_type(ctx, jsx_el);
if !DEFAULT_COMPONENTS.iter().any(|&comp| comp == name)
&& !self

View file

@ -61,9 +61,7 @@ impl Rule for HtmlHasLang {
return;
};
let Some(element_type) = get_element_type(ctx, jsx_el) else {
return;
};
let element_type = get_element_type(ctx, jsx_el);
if element_type != "html" {
return;

View file

@ -66,9 +66,7 @@ impl Rule for IframeHasTitle {
return;
};
let Some(name) = get_element_type(ctx, jsx_el) else {
return;
};
let name = get_element_type(ctx, jsx_el);
if name != "iframe" {
return;

View file

@ -125,9 +125,8 @@ impl Rule for ImgRedundantAlt {
let AstKind::JSXOpeningElement(jsx_el) = node.kind() else {
return;
};
let Some(element_type) = get_element_type(ctx, jsx_el) else {
return;
};
let element_type = get_element_type(ctx, jsx_el);
if !self.types_to_validate.iter().any(|comp| comp == &element_type) {
return;

View file

@ -206,9 +206,7 @@ impl Rule for LabelHasAssociatedControl {
return;
};
let Some(element_type) = get_element_type(ctx, &element.opening_element) else {
return;
};
let element_type = get_element_type(ctx, &element.opening_element);
if self.label_components.binary_search(&element_type.into()).is_err() {
return;
@ -295,10 +293,9 @@ impl LabelHasAssociatedControl {
match node {
JSXChild::ExpressionContainer(_) => true,
JSXChild::Element(element) => {
if let Some(element_type) = get_element_type(ctx, &element.opening_element) {
if self.control_components.is_match(element_type.to_string()) {
return true;
}
let element_type = get_element_type(ctx, &element.opening_element);
if self.control_components.is_match(element_type.to_string()) {
return true;
}
for child in &element.children {
@ -359,12 +356,11 @@ impl LabelHasAssociatedControl {
}
if element.children.is_empty() {
if let Some(name) = get_element_type(ctx, &element.opening_element) {
if is_react_component_name(&name)
&& !self.control_components.is_match(name.to_string())
{
return true;
}
let name = get_element_type(ctx, &element.opening_element);
if is_react_component_name(&name)
&& !self.control_components.is_match(name.to_string())
{
return true;
}
}
@ -1589,6 +1585,13 @@ fn test() {
}])),
None,
),
(
"<FilesContext.Provider value={{ addAlert, cwdInfo }} />",
Some(serde_json::json!([{
"labelComponents": ["FilesContext.Provider"],
}])),
None,
),
];
Tester::new(LabelHasAssociatedControl::NAME, LabelHasAssociatedControl::CATEGORY, pass, fail)

View file

@ -63,9 +63,7 @@ impl Rule for Lang {
return;
};
let Some(element_type) = get_element_type(ctx, jsx_el) else {
return;
};
let element_type = get_element_type(ctx, jsx_el);
if element_type != "html" {
return;

View file

@ -110,9 +110,7 @@ impl Rule for MediaHasCaption {
return;
};
let Some(element_name) = get_element_type(ctx, jsx_el) else {
return;
};
let element_name = get_element_type(ctx, jsx_el);
let is_audio_or_video =
self.0.audio.contains(&element_name) || self.0.video.contains(&element_name);
@ -158,9 +156,8 @@ impl Rule for MediaHasCaption {
} else {
parent.children.iter().any(|child| match child {
JSXChild::Element(child_el) => {
let Some(child_name) = get_element_type(ctx, &child_el.opening_element) else {
return false;
};
let child_name = get_element_type(ctx, &child_el.opening_element);
self.0.track.contains(&child_name)
&& child_el.opening_element.attributes.iter().any(|attr| {
if let JSXAttributeItem::Attribute(attr) = attr {

View file

@ -102,9 +102,7 @@ impl Rule for MouseEventsHaveKeyEvents {
return;
};
let Some(el_type) = get_element_type(ctx, jsx_opening_el) else {
return;
};
let el_type = get_element_type(ctx, jsx_opening_el);
if !HTML_TAG.contains(&el_type) {
return;

View file

@ -92,9 +92,7 @@ fn is_aria_hidden_true(attr: &JSXAttributeItem) -> bool {
///
/// `true` if the element is focusable, `false` otherwise.
fn is_focusable<'a>(ctx: &LintContext<'a>, element: &JSXOpeningElement<'a>) -> bool {
let Some(tag_name) = get_element_type(ctx, element) else {
return false;
};
let tag_name = get_element_type(ctx, element);
if let Some(JSXAttributeItem::Attribute(attr)) = has_jsx_prop_ignore_case(element, "tabIndex") {
if let Some(attr_value) = &attr.value {

View file

@ -100,9 +100,8 @@ impl Rule for NoAutofocus {
let Some(autofocus) = has_jsx_prop(&jsx_el.opening_element, "autoFocus") else {
return;
};
let Some(element_type) = get_element_type(ctx, &jsx_el.opening_element) else {
return;
};
let element_type = get_element_type(ctx, &jsx_el.opening_element);
if self.ignore_non_dom {
if HTML_TAG.contains(&element_type) {

View file

@ -58,9 +58,8 @@ impl Rule for NoDistractingElements {
let AstKind::JSXOpeningElement(jsx_el) = node.kind() else {
return;
};
let Some(element_type) = get_element_type(ctx, jsx_el) else {
return;
};
let element_type = get_element_type(ctx, jsx_el);
if let "marquee" | "blink" = element_type.as_ref() {
ctx.diagnostic(no_distracting_elements_diagnostic(jsx_el.name.span()));

View file

@ -61,9 +61,8 @@ impl Rule for NoRedundantRoles {
let AstKind::JSXOpeningElement(jsx_el) = node.kind() else {
return;
};
let Some(component) = get_element_type(ctx, jsx_el) else {
return;
};
let component = get_element_type(ctx, jsx_el);
if let Some(JSXAttributeItem::Attribute(attr)) = has_jsx_prop_ignore_case(jsx_el, "role") {
if let Some(JSXAttributeValue::StringLiteral(role_values)) = &attr.value {

View file

@ -92,10 +92,9 @@ lazy_static! {
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(name) = get_element_type(ctx, jsx_el) {
if let Some(role_prop) = has_jsx_prop_ignore_case(jsx_el, "role") {
Self::check_roles(role_prop, &ROLE_TO_TAG_MAP, &name, ctx);
}
let name = get_element_type(ctx, jsx_el);
if let Some(role_prop) = has_jsx_prop_ignore_case(jsx_el, "role") {
Self::check_roles(role_prop, &ROLE_TO_TAG_MAP, &name, ctx);
}
}
}

View file

@ -68,9 +68,8 @@ impl Rule for RoleSupportsAriaProps {
let AstKind::JSXOpeningElement(jsx_el) = node.kind() else {
return;
};
let Some(el_type) = get_element_type(ctx, jsx_el) else {
return;
};
let el_type = get_element_type(ctx, jsx_el);
let role = has_jsx_prop_ignore_case(jsx_el, "role");
let role_value = role.map_or_else(

View file

@ -65,9 +65,7 @@ impl Rule for Scope {
}
};
let Some(element_type) = get_element_type(ctx, jsx_el) else {
return;
};
let element_type = get_element_type(ctx, jsx_el);
if element_type == "th" {
return;

View file

@ -69,9 +69,8 @@ impl Rule for CheckedRequiresOnchangeOrReadonly {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
AstKind::JSXOpeningElement(jsx_opening_el) => {
let Some(element_type) = get_element_type(ctx, jsx_opening_el) else {
return;
};
let element_type = get_element_type(ctx, jsx_opening_el);
if element_type != "input" {
return;
}

View file

@ -616,3 +616,10 @@ snapshot_kind: text
· ────────────────────────────────────
╰────
help: Either give the label a `htmlFor` attribute with the id of the associated control, or wrap the label around the control.
⚠ eslint-plugin-jsx-a11y(label-has-associated-control): A form label must have accessible text.
╭─[label_has_associated_control.tsx:1:1]
1 │ <FilesContext.Provider value={{ addAlert, cwdInfo }} />
· ───────────────────────────────────────────────────────
╰────
help: Ensure the label either has text inside it or is accessibly labelled using an attribute such as `aria-label`, or `aria-labelledby`. You can mark more attributes as accessible labels by configuring the `labelAttributes` option.

View file

@ -3,7 +3,8 @@ use std::borrow::Cow;
use oxc_ast::{
ast::{
CallExpression, Expression, JSXAttributeItem, JSXAttributeName, JSXAttributeValue,
JSXChild, JSXElement, JSXExpression, JSXOpeningElement, MemberExpression,
JSXChild, JSXElement, JSXElementName, JSXExpression, JSXMemberExpression,
JSXMemberExpressionObject, JSXOpeningElement, MemberExpression,
},
match_member_expression, AstKind,
};
@ -65,14 +66,13 @@ pub fn is_hidden_from_screen_reader<'a>(
ctx: &LintContext<'a>,
node: &JSXOpeningElement<'a>,
) -> bool {
if let Some(name) = get_element_type(ctx, node) {
if name.eq_ignore_ascii_case("input") {
if let Some(item) = has_jsx_prop_ignore_case(node, "type") {
let hidden = get_string_literal_prop_value(item);
let name = get_element_type(ctx, node);
if name.eq_ignore_ascii_case("input") {
if let Some(item) = has_jsx_prop_ignore_case(node, "type") {
let hidden = get_string_literal_prop_value(item);
if hidden.is_some_and(|val| val.eq_ignore_ascii_case("hidden")) {
return true;
}
if hidden.is_some_and(|val| val.eq_ignore_ascii_case("hidden")) {
return true;
}
}
}
@ -204,14 +204,34 @@ pub fn get_parent_component<'a, 'b>(
None
}
fn get_jsx_mem_expr_name<'a>(jsx_mem_expr: &JSXMemberExpression) -> Cow<'a, str> {
let prefix = match &jsx_mem_expr.object {
JSXMemberExpressionObject::IdentifierReference(id) => Cow::Borrowed(id.name.as_str()),
JSXMemberExpressionObject::MemberExpression(mem_expr) => {
Cow::Owned(format!("{}.{}", get_jsx_mem_expr_name(mem_expr), mem_expr.property.name))
}
JSXMemberExpressionObject::ThisExpression(_) => Cow::Borrowed("this"),
};
Cow::Owned(format!("{}.{}", prefix, jsx_mem_expr.property.name))
}
/// Resolve element type(name) using jsx-a11y settings
/// ref:
/// <https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.9.0/src/util/getElementType.js>
pub fn get_element_type<'c, 'a>(
context: &'c LintContext<'a>,
element: &JSXOpeningElement<'a>,
) -> Option<Cow<'c, str>> {
let name = element.name.get_identifier_name()?;
) -> Cow<'c, str> {
let name = match &element.name {
JSXElementName::Identifier(id) => Cow::Borrowed(id.as_ref().name.as_str()),
JSXElementName::IdentifierReference(id) => Cow::Borrowed(id.as_ref().name.as_str()),
JSXElementName::NamespacedName(namespaced) => {
Cow::Owned(format!("{}:{}", namespaced.namespace.name, namespaced.property.name))
}
JSXElementName::MemberExpression(jsx_mem_expr) => get_jsx_mem_expr_name(jsx_mem_expr),
JSXElementName::ThisExpression(_) => Cow::Borrowed("this"),
};
let OxlintSettings { jsx_a11y, .. } = context.settings();
@ -225,10 +245,10 @@ pub fn get_element_type<'c, 'a>(
.and_then(JSXAttributeValue::as_string_literal)
.map(|s| s.value.as_str());
let raw_type = polymorphic_prop.unwrap_or_else(|| name.as_str());
match jsx_a11y.components.get(raw_type) {
Some(component) => Some(Cow::Borrowed(component)),
None => Some(Cow::Borrowed(raw_type)),
let raw_type = polymorphic_prop.map_or(name, Cow::Borrowed);
match jsx_a11y.components.get(raw_type.as_ref()) {
Some(component) => Cow::Borrowed(component),
None => raw_type,
}
}