mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(linter/jsx-a11y/mouse-events-have-key-events): handle nullish event handlers (#27306)
`mouse-events-have-key-events` reports absent hover handlers such as
`onMouseOver={undefined}` and misses active hover handlers paired with
`onFocus={null}`. The same issue affects hover-out/blur and configured
handler lists.
Use one local predicate to skip nullish hover values, reject nullish
keyboard values, and continue to a later configured active hover
handler. Preserve existing component, spread and bare-attribute
behavior. Bare attributes retain Oxc's prior behavior, which differs
from [the upstream
rule](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.10.2/src/rules/mouse-events-have-key-events.js).
Validation:
- The new regression test demonstrated five false-positive cases and
four false-negative cases before the fix.
- Both focused rule tests pass, with 22 new regression/control cases
including default/configured handlers, ordering, dynamic functions,
components and bare attributes.
- Independent AI code review found no blocking issues; suggested
bare-attribute coverage was added.
- Full post-commit `just ready` passed: 98 test targets, 5,061 passed /
161 ignored / 0 failed; formatting, code generation, checks, Clippy and
documentation completed with a clean worktree.
Fixes #27305
AI-assisted with Codex.
---------
Co-authored-by: Pybsama <294532706+Pybsama@users.noreply.github.com>
Co-authored-by: Cameron Clark <cameron.clark@hey.com>
This commit is contained in:
2 files changed
+124
-25
No files matched your search
@@ -1,4 +1,7 @@
|
||||
use oxc_ast::{AstKind, ast::JSXAttributeValue};
|
||||
use oxc_ast::{
|
||||
AstKind,
|
||||
ast::{Expression, JSXAttributeItem, JSXAttributeValue},
|
||||
};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
@@ -96,22 +99,12 @@ impl Rule for MouseEventsHaveKeyEvents {
|
||||
|
||||
for handler in &self.0.hover_in_handlers {
|
||||
if let Some(jsx_attr) = has_jsx_prop(jsx_opening_el, handler) {
|
||||
if get_prop_value(jsx_attr).is_none() {
|
||||
if !has_handler_value(jsx_attr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match has_jsx_prop(jsx_opening_el, "onFocus").and_then(get_prop_value) {
|
||||
Some(JSXAttributeValue::ExpressionContainer(container)) => {
|
||||
if let Some(expr) = container.expression.as_expression()
|
||||
&& expr.is_undefined()
|
||||
{
|
||||
ctx.diagnostic(miss_on_focus(jsx_attr.span(), handler));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ctx.diagnostic(miss_on_focus(jsx_attr.span(), handler));
|
||||
}
|
||||
_ => {}
|
||||
if !has_jsx_prop(jsx_opening_el, "onFocus").is_some_and(has_handler_value) {
|
||||
ctx.diagnostic(miss_on_focus(jsx_attr.span(), handler));
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -120,20 +113,12 @@ impl Rule for MouseEventsHaveKeyEvents {
|
||||
|
||||
for handler in &self.0.hover_out_handlers {
|
||||
if let Some(jsx_attr) = has_jsx_prop(jsx_opening_el, handler) {
|
||||
if get_prop_value(jsx_attr).is_none() {
|
||||
if !has_handler_value(jsx_attr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match has_jsx_prop(jsx_opening_el, "onBlur").and_then(get_prop_value) {
|
||||
Some(JSXAttributeValue::ExpressionContainer(container))
|
||||
if container.expression.is_undefined() =>
|
||||
{
|
||||
ctx.diagnostic(miss_on_blur(jsx_attr.span(), handler));
|
||||
}
|
||||
None => {
|
||||
ctx.diagnostic(miss_on_blur(jsx_attr.span(), handler));
|
||||
}
|
||||
_ => {}
|
||||
if !has_jsx_prop(jsx_opening_el, "onBlur").is_some_and(has_handler_value) {
|
||||
ctx.diagnostic(miss_on_blur(jsx_attr.span(), handler));
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -142,10 +127,29 @@ impl Rule for MouseEventsHaveKeyEvents {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_handler_value(attribute: &JSXAttributeItem<'_>) -> bool {
|
||||
match get_prop_value(attribute) {
|
||||
Some(JSXAttributeValue::ExpressionContainer(container)) => {
|
||||
let Some(expression) = container.expression.as_expression() else {
|
||||
return true;
|
||||
};
|
||||
let expression = expression.get_inner_expression();
|
||||
!expression.is_undefined() && !matches!(expression, Expression::NullLiteral(_))
|
||||
}
|
||||
Some(_) => true,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let configured = Some(serde_json::json!([{
|
||||
"hoverInHandlers": ["onMouseOver", "onMouseEnter"],
|
||||
"hoverOutHandlers": ["onMouseOut", "onMouseLeave"]
|
||||
}]));
|
||||
|
||||
let pass = vec![
|
||||
("<div onMouseOver={() => void 0} onFocus={() => void 0} />;", None),
|
||||
("<div onMouseOver={() => void 0} onFocus={() => void 0} {...props} />;", None),
|
||||
@@ -197,6 +201,21 @@ fn test() {
|
||||
"<div onMouseLeave={() => {}} />",
|
||||
Some(serde_json::json!([{ "hoverOutHandlers": ["onPointerLeave"] }])),
|
||||
),
|
||||
("<div onMouseOver={undefined} />", None),
|
||||
("<div onMouseOut={undefined} />", None),
|
||||
("<div onMouseOver={null} />", None),
|
||||
("<div onMouseOut={null} />", None),
|
||||
("<div onMouseOver={undefined} onMouseOut={null} />", None),
|
||||
("<div onMouseOver />", None),
|
||||
("<div onMouseOut />", None),
|
||||
("<div onMouseOver={handler} onFocus={handler} />", None),
|
||||
("<div onMouseOut={handler} onBlur={handler} />", None),
|
||||
("<div onMouseOver={null} onMouseEnter={handler} onFocus={handler} />", configured.clone()),
|
||||
(
|
||||
"<div onMouseOut={undefined} onMouseLeave={handler} onBlur={handler} />",
|
||||
configured.clone(),
|
||||
),
|
||||
("<Custom onMouseOver={handler} onFocus={null} />", None),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
@@ -234,6 +253,16 @@ fn test() {
|
||||
"<div onPointerLeave={() => {}} />",
|
||||
Some(serde_json::json!([{ "hoverOutHandlers": ["onPointerLeave"] }])),
|
||||
),
|
||||
("<div onMouseOver={handler} onFocus={null} />", None),
|
||||
("<div onMouseOut={handler} onBlur={null} />", None),
|
||||
("<div onMouseOver={handler} onFocus={undefined} />", None),
|
||||
("<div onMouseOut={handler} onBlur={undefined} />", None),
|
||||
("<div onMouseOver={handler} onFocus />", None),
|
||||
("<div onMouseOut={handler} onBlur />", None),
|
||||
("<div onMouseOver={null} onMouseEnter={handler} />", configured.clone()),
|
||||
("<div onMouseOut={undefined} onMouseLeave={handler} />", configured.clone()),
|
||||
("<div onMouseOver={null} onMouseEnter={handler} onFocus={null} />", configured.clone()),
|
||||
("<div onMouseOut={undefined} onMouseLeave={handler} onBlur={null} />", configured),
|
||||
];
|
||||
|
||||
Tester::new(MouseEventsHaveKeyEvents::NAME, MouseEventsHaveKeyEvents::PLUGIN, pass, fail)
|
||||
|
||||
@@ -99,3 +99,73 @@ source: crates/oxc_linter/src/tester.rs
|
||||
· ─────────────────────────
|
||||
╰────
|
||||
help: Try to add `onBlur`.
|
||||
|
||||
⚠ jsx-a11y(mouse-events-have-key-events): `onMouseOver` must be accompanied by `onFocus` for accessibility.
|
||||
╭─[mouse_events_have_key_events.tsx:1:6]
|
||||
1 │ <div onMouseOver={handler} onFocus={null} />
|
||||
· ─────────────────────
|
||||
╰────
|
||||
help: Try to add `onFocus`.
|
||||
|
||||
⚠ jsx-a11y(mouse-events-have-key-events): `onMouseOut` must be accompanied by `onBlur` for accessibility.
|
||||
╭─[mouse_events_have_key_events.tsx:1:6]
|
||||
1 │ <div onMouseOut={handler} onBlur={null} />
|
||||
· ────────────────────
|
||||
╰────
|
||||
help: Try to add `onBlur`.
|
||||
|
||||
⚠ jsx-a11y(mouse-events-have-key-events): `onMouseOver` must be accompanied by `onFocus` for accessibility.
|
||||
╭─[mouse_events_have_key_events.tsx:1:6]
|
||||
1 │ <div onMouseOver={handler} onFocus={undefined} />
|
||||
· ─────────────────────
|
||||
╰────
|
||||
help: Try to add `onFocus`.
|
||||
|
||||
⚠ jsx-a11y(mouse-events-have-key-events): `onMouseOut` must be accompanied by `onBlur` for accessibility.
|
||||
╭─[mouse_events_have_key_events.tsx:1:6]
|
||||
1 │ <div onMouseOut={handler} onBlur={undefined} />
|
||||
· ────────────────────
|
||||
╰────
|
||||
help: Try to add `onBlur`.
|
||||
|
||||
⚠ jsx-a11y(mouse-events-have-key-events): `onMouseOver` must be accompanied by `onFocus` for accessibility.
|
||||
╭─[mouse_events_have_key_events.tsx:1:6]
|
||||
1 │ <div onMouseOver={handler} onFocus />
|
||||
· ─────────────────────
|
||||
╰────
|
||||
help: Try to add `onFocus`.
|
||||
|
||||
⚠ jsx-a11y(mouse-events-have-key-events): `onMouseOut` must be accompanied by `onBlur` for accessibility.
|
||||
╭─[mouse_events_have_key_events.tsx:1:6]
|
||||
1 │ <div onMouseOut={handler} onBlur />
|
||||
· ────────────────────
|
||||
╰────
|
||||
help: Try to add `onBlur`.
|
||||
|
||||
⚠ jsx-a11y(mouse-events-have-key-events): `onMouseEnter` must be accompanied by `onFocus` for accessibility.
|
||||
╭─[mouse_events_have_key_events.tsx:1:25]
|
||||
1 │ <div onMouseOver={null} onMouseEnter={handler} />
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Try to add `onFocus`.
|
||||
|
||||
⚠ jsx-a11y(mouse-events-have-key-events): `onMouseLeave` must be accompanied by `onBlur` for accessibility.
|
||||
╭─[mouse_events_have_key_events.tsx:1:29]
|
||||
1 │ <div onMouseOut={undefined} onMouseLeave={handler} />
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Try to add `onBlur`.
|
||||
|
||||
⚠ jsx-a11y(mouse-events-have-key-events): `onMouseEnter` must be accompanied by `onFocus` for accessibility.
|
||||
╭─[mouse_events_have_key_events.tsx:1:25]
|
||||
1 │ <div onMouseOver={null} onMouseEnter={handler} onFocus={null} />
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Try to add `onFocus`.
|
||||
|
||||
⚠ jsx-a11y(mouse-events-have-key-events): `onMouseLeave` must be accompanied by `onBlur` for accessibility.
|
||||
╭─[mouse_events_have_key_events.tsx:1:29]
|
||||
1 │ <div onMouseOut={undefined} onMouseLeave={handler} onBlur={null} />
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Try to add `onBlur`.
|
||||
Reference in new issue
Block a user