mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
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:
parent
7637aac21f
commit
ee26b448cc
27 changed files with 103 additions and 111 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue