From fba34105c791c276ef2915bc9678c4fc9f9e8844 Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Thu, 23 Oct 2025 08:53:09 -0500 Subject: [PATCH] feat(linter): adds jsx-a11y/no-static-element-interactions rule * includes codegen for npm `aria-query` & `axobject-query` data required for this and other jsx-a11y rule implementations --- Cargo.lock | 21 + Cargo.toml | 1 + crates/oxc_aria_query/Cargo.toml | 22 + crates/oxc_aria_query/src/lib.rs | 771 +++++++++++++++ crates/oxc_linter/Cargo.toml | 1 + .../src/generated/rule_runner_impls.rs | 6 + crates/oxc_linter/src/rules.rs | 2 + .../no_static_element_interactions.rs | 273 ++++++ ...x_a11y_no_static_element_interactions.snap | 51 + crates/oxc_linter/src/utils/react.rs | 130 ++- pnpm-lock.yaml | 928 +++++++++--------- pnpm-workspace.yaml | 1 + tasks/a11y_data/.gitignore | 1 + tasks/a11y_data/Cargo.toml | 25 + tasks/a11y_data/README | 17 + tasks/a11y_data/abstractRoles.json | 14 + tasks/a11y_data/init.js | 131 +++ .../interactiveElementRoleSchemas.json | 363 +++++++ tasks/a11y_data/interactiveRoles.json | 36 + .../noninteractiveAxObjectSchema.json | 178 ++++ .../noninteractiveElementRoleSchemas.json | 302 ++++++ tasks/a11y_data/noninteractiveRoles.json | 94 ++ tasks/a11y_data/package.json | 11 + tasks/a11y_data/src/lib.rs | 140 +++ tasks/a11y_data/src/main.rs | 21 + 25 files changed, 3086 insertions(+), 454 deletions(-) create mode 100644 crates/oxc_aria_query/Cargo.toml create mode 100644 crates/oxc_aria_query/src/lib.rs create mode 100644 crates/oxc_linter/src/rules/jsx_a11y/no_static_element_interactions.rs create mode 100644 crates/oxc_linter/src/snapshots/jsx_a11y_no_static_element_interactions.snap create mode 100644 tasks/a11y_data/.gitignore create mode 100644 tasks/a11y_data/Cargo.toml create mode 100644 tasks/a11y_data/README create mode 100644 tasks/a11y_data/abstractRoles.json create mode 100644 tasks/a11y_data/init.js create mode 100644 tasks/a11y_data/interactiveElementRoleSchemas.json create mode 100644 tasks/a11y_data/interactiveRoles.json create mode 100644 tasks/a11y_data/noninteractiveAxObjectSchema.json create mode 100644 tasks/a11y_data/noninteractiveElementRoleSchemas.json create mode 100644 tasks/a11y_data/noninteractiveRoles.json create mode 100644 tasks/a11y_data/package.json create mode 100644 tasks/a11y_data/src/lib.rs create mode 100644 tasks/a11y_data/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 565254f238a73..94ed37b52c20b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1700,6 +1700,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "oxc_a11y_data" +version = "0.1.0" +dependencies = [ + "oxc_tasks_common", + "prettyplease", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + [[package]] name = "oxc_allocator" version = "0.95.0" @@ -1728,6 +1741,13 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "oxc_aria_query" +version = "0.95.0" +dependencies = [ + "phf", +] + [[package]] name = "oxc_ast" version = "0.95.0" @@ -2157,6 +2177,7 @@ dependencies = [ "memchr", "oxc-schemars", "oxc_allocator 0.95.0", + "oxc_aria_query", "oxc_ast 0.95.0", "oxc_ast_macros 0.95.0", "oxc_ast_visit 0.95.0", diff --git a/Cargo.toml b/Cargo.toml index 5a692007603b4..2694edee7d508 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ multiple_crate_versions = "allow" # publish = true oxc = { version = "0.95.0", path = "crates/oxc" } oxc_allocator = { version = "0.95.0", path = "crates/oxc_allocator" } +oxc_aria_query = { version = "0.95.0", path = "crates/oxc_aria_query" } oxc_ast = { version = "0.95.0", path = "crates/oxc_ast" } oxc_ast_macros = { version = "0.95.0", path = "crates/oxc_ast_macros" } oxc_ast_visit = { version = "0.95.0", path = "crates/oxc_ast_visit" } diff --git a/crates/oxc_aria_query/Cargo.toml b/crates/oxc_aria_query/Cargo.toml new file mode 100644 index 0000000000000..1a1d40a65694c --- /dev/null +++ b/crates/oxc_aria_query/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "oxc_aria_query" +version = "0.95.0" +authors.workspace = true +categories.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +description.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lib] +test = false +doctest = false + +[dependencies] +phf = { workspace = true, features = ["macros"] } + +[lints] +workspace = true diff --git a/crates/oxc_aria_query/src/lib.rs b/crates/oxc_aria_query/src/lib.rs new file mode 100644 index 0000000000000..d78da0120c17e --- /dev/null +++ b/crates/oxc_aria_query/src/lib.rs @@ -0,0 +1,771 @@ +/// Auto generated by `tasks/a11y_data/src/lib.rs`. +use phf::phf_set; +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct Attr { + pub name: &'static str, + pub value: Option<&'static str>, +} +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct ElementSchema { + pub name: &'static str, + pub attributes: &'static [Attr], +} +pub static ABSTRACT_ROLES: phf::Set<&'static str> = phf_set! { + "command", "composite", "input", "landmark", "range", "roletype", "section", + "sectionhead", "select", "structure", "widget", "window", +}; + +pub static INTERACTIVE_ROLES: phf::Set<&'static str> = phf_set! { + "button", "checkbox", "columnheader", "combobox", "doc-backlink", "doc-biblioref", + "doc-glossref", "doc-noteref", "grid", "gridcell", "link", "listbox", "menu", + "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", + "radiogroup", "row", "rowheader", "scrollbar", "searchbox", "slider", "spinbutton", + "switch", "tab", "tablist", "textbox", "tree", "treegrid", "treeitem", "toolbar", +}; + +pub static NONINTERACTIVE_ROLES: phf::Set<&'static str> = phf_set! { + "alert", "alertdialog", "application", "article", "banner", "blockquote", "caption", + "cell", "code", "complementary", "contentinfo", "definition", "deletion", "dialog", + "directory", "doc-abstract", "doc-acknowledgments", "doc-afterword", "doc-appendix", + "doc-biblioentry", "doc-bibliography", "doc-chapter", "doc-colophon", + "doc-conclusion", "doc-cover", "doc-credit", "doc-credits", "doc-dedication", + "doc-endnote", "doc-endnotes", "doc-epigraph", "doc-epilogue", "doc-errata", + "doc-example", "doc-footnote", "doc-foreword", "doc-glossary", "doc-index", + "doc-introduction", "doc-notice", "doc-pagebreak", "doc-pagefooter", + "doc-pageheader", "doc-pagelist", "doc-part", "doc-preface", "doc-prologue", + "doc-pullquote", "doc-qna", "doc-subtitle", "doc-tip", "doc-toc", "document", + "emphasis", "feed", "figure", "form", "graphics-document", "graphics-object", + "graphics-symbol", "group", "heading", "img", "insertion", "list", "listitem", "log", + "main", "mark", "marquee", "math", "meter", "navigation", "none", "note", + "paragraph", "presentation", "region", "rowgroup", "search", "separator", "status", + "strong", "subscript", "superscript", "table", "tabpanel", "term", "time", "timer", + "tooltip", "progressbar", +}; + +pub static INTERACTIVE_ELEMENT_SCHEMA: &[ElementSchema] = &[ + ElementSchema { + name: "a", + attributes: &[Attr { name: "href", value: None }], + }, + ElementSchema { + name: "area", + attributes: &[Attr { name: "href", value: None }], + }, + ElementSchema { + name: "button", + attributes: &[], + }, + ElementSchema { + name: "datalist", + attributes: &[], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { + name: "type", + value: Some("button"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { + name: "type", + value: Some("image"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { + name: "type", + value: Some("reset"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { + name: "type", + value: Some("submit"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { + name: "type", + value: Some("checkbox"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "list", value: None }, + Attr { + name: "type", + value: Some("email"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "list", value: None }, + Attr { + name: "type", + value: Some("search"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "list", value: None }, + Attr { + name: "type", + value: Some("tel"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "list", value: None }, + Attr { + name: "type", + value: Some("text"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "list", value: None }, + Attr { + name: "type", + value: Some("url"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { + name: "type", + value: Some("radio"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "list", value: None }, + Attr { + name: "type", + value: Some("search"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { + name: "type", + value: Some("range"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { + name: "type", + value: Some("number"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "type", value: None }, + Attr { name: "list", value: None }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "list", value: None }, + Attr { + name: "type", + value: Some("email"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "list", value: None }, + Attr { + name: "type", + value: Some("tel"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "list", value: None }, + Attr { + name: "type", + value: Some("text"), + }, + ], + }, + ElementSchema { + name: "input", + attributes: &[ + Attr { name: "list", value: None }, + Attr { + name: "type", + value: Some("url"), + }, + ], + }, + ElementSchema { + name: "option", + attributes: &[], + }, + ElementSchema { + name: "select", + attributes: &[ + Attr { + name: "multiple", + value: None, + }, + Attr { name: "size", value: None }, + ], + }, + ElementSchema { + name: "select", + attributes: &[Attr { name: "size", value: None }], + }, + ElementSchema { + name: "select", + attributes: &[ + Attr { + name: "multiple", + value: None, + }, + ], + }, + ElementSchema { + name: "td", + attributes: &[], + }, + ElementSchema { + name: "textarea", + attributes: &[], + }, + ElementSchema { + name: "th", + attributes: &[], + }, + ElementSchema { + name: "th", + attributes: &[ + Attr { + name: "scope", + value: Some("col"), + }, + ], + }, + ElementSchema { + name: "th", + attributes: &[ + Attr { + name: "scope", + value: Some("colgroup"), + }, + ], + }, + ElementSchema { + name: "th", + attributes: &[ + Attr { + name: "scope", + value: Some("row"), + }, + ], + }, + ElementSchema { + name: "th", + attributes: &[ + Attr { + name: "scope", + value: Some("rowgroup"), + }, + ], + }, + ElementSchema { + name: "tr", + attributes: &[], + }, +]; + +pub static NONINTERACTIVE_ELEMENT_SCHEMA: &[ElementSchema] = &[ + ElementSchema { + name: "address", + attributes: &[], + }, + ElementSchema { + name: "article", + attributes: &[], + }, + ElementSchema { + name: "aside", + attributes: &[], + }, + ElementSchema { + name: "aside", + attributes: &[ + Attr { + name: "aria-label", + value: None, + }, + ], + }, + ElementSchema { + name: "aside", + attributes: &[ + Attr { + name: "aria-labelledby", + value: None, + }, + ], + }, + ElementSchema { + name: "blockquote", + attributes: &[], + }, + ElementSchema { + name: "caption", + attributes: &[], + }, + ElementSchema { + name: "code", + attributes: &[], + }, + ElementSchema { + name: "dd", + attributes: &[], + }, + ElementSchema { + name: "del", + attributes: &[], + }, + ElementSchema { + name: "details", + attributes: &[], + }, + ElementSchema { + name: "dfn", + attributes: &[], + }, + ElementSchema { + name: "dialog", + attributes: &[], + }, + ElementSchema { + name: "dt", + attributes: &[], + }, + ElementSchema { + name: "em", + attributes: &[], + }, + ElementSchema { + name: "fieldset", + attributes: &[], + }, + ElementSchema { + name: "figure", + attributes: &[], + }, + ElementSchema { + name: "footer", + attributes: &[], + }, + ElementSchema { + name: "form", + attributes: &[ + Attr { + name: "aria-label", + value: None, + }, + ], + }, + ElementSchema { + name: "form", + attributes: &[ + Attr { + name: "aria-labelledby", + value: None, + }, + ], + }, + ElementSchema { + name: "form", + attributes: &[Attr { name: "name", value: None }], + }, + ElementSchema { + name: "h1", + attributes: &[], + }, + ElementSchema { + name: "h2", + attributes: &[], + }, + ElementSchema { + name: "h3", + attributes: &[], + }, + ElementSchema { + name: "h4", + attributes: &[], + }, + ElementSchema { + name: "h5", + attributes: &[], + }, + ElementSchema { + name: "h6", + attributes: &[], + }, + ElementSchema { + name: "header", + attributes: &[], + }, + ElementSchema { + name: "hr", + attributes: &[], + }, + ElementSchema { + name: "html", + attributes: &[], + }, + ElementSchema { + name: "img", + attributes: &[Attr { name: "alt", value: None }], + }, + ElementSchema { + name: "img", + attributes: &[Attr { name: "alt", value: None }], + }, + ElementSchema { + name: "img", + attributes: &[ + Attr { + name: "alt", + value: Some(""), + }, + ], + }, + ElementSchema { + name: "ins", + attributes: &[], + }, + ElementSchema { + name: "li", + attributes: &[], + }, + ElementSchema { + name: "main", + attributes: &[], + }, + ElementSchema { + name: "mark", + attributes: &[], + }, + ElementSchema { + name: "math", + attributes: &[], + }, + ElementSchema { + name: "menu", + attributes: &[], + }, + ElementSchema { + name: "meter", + attributes: &[], + }, + ElementSchema { + name: "nav", + attributes: &[], + }, + ElementSchema { + name: "ol", + attributes: &[], + }, + ElementSchema { + name: "optgroup", + attributes: &[], + }, + ElementSchema { + name: "output", + attributes: &[], + }, + ElementSchema { + name: "p", + attributes: &[], + }, + ElementSchema { + name: "progress", + attributes: &[], + }, + ElementSchema { + name: "section", + attributes: &[ + Attr { + name: "aria-label", + value: None, + }, + ], + }, + ElementSchema { + name: "section", + attributes: &[ + Attr { + name: "aria-labelledby", + value: None, + }, + ], + }, + ElementSchema { + name: "strong", + attributes: &[], + }, + ElementSchema { + name: "sub", + attributes: &[], + }, + ElementSchema { + name: "sup", + attributes: &[], + }, + ElementSchema { + name: "table", + attributes: &[], + }, + ElementSchema { + name: "tbody", + attributes: &[], + }, + ElementSchema { + name: "td", + attributes: &[], + }, + ElementSchema { + name: "tfoot", + attributes: &[], + }, + ElementSchema { + name: "thead", + attributes: &[], + }, + ElementSchema { + name: "time", + attributes: &[], + }, + ElementSchema { + name: "ul", + attributes: &[], + }, +]; + +pub static NONINTERACTIVE_AXOBJECT_ELEMENT_SCHEMA: &[ElementSchema] = &[ + ElementSchema { + name: "abbr", + attributes: &[], + }, + ElementSchema { + name: "article", + attributes: &[], + }, + ElementSchema { + name: "blockquote", + attributes: &[], + }, + ElementSchema { + name: "br", + attributes: &[], + }, + ElementSchema { + name: "caption", + attributes: &[], + }, + ElementSchema { + name: "dd", + attributes: &[], + }, + ElementSchema { + name: "details", + attributes: &[], + }, + ElementSchema { + name: "dfn", + attributes: &[], + }, + ElementSchema { + name: "dialog", + attributes: &[], + }, + ElementSchema { + name: "dir", + attributes: &[], + }, + ElementSchema { + name: "dl", + attributes: &[], + }, + ElementSchema { + name: "dt", + attributes: &[], + }, + ElementSchema { + name: "figcaption", + attributes: &[], + }, + ElementSchema { + name: "figure", + attributes: &[], + }, + ElementSchema { + name: "footer", + attributes: &[], + }, + ElementSchema { + name: "form", + attributes: &[], + }, + ElementSchema { + name: "h1", + attributes: &[], + }, + ElementSchema { + name: "h2", + attributes: &[], + }, + ElementSchema { + name: "h3", + attributes: &[], + }, + ElementSchema { + name: "h4", + attributes: &[], + }, + ElementSchema { + name: "h5", + attributes: &[], + }, + ElementSchema { + name: "h6", + attributes: &[], + }, + ElementSchema { + name: "iframe", + attributes: &[], + }, + ElementSchema { + name: "img", + attributes: &[ + Attr { + name: "usemap", + value: None, + }, + ], + }, + ElementSchema { + name: "img", + attributes: &[], + }, + ElementSchema { + name: "label", + attributes: &[], + }, + ElementSchema { + name: "legend", + attributes: &[], + }, + ElementSchema { + name: "li", + attributes: &[], + }, + ElementSchema { + name: "main", + attributes: &[], + }, + ElementSchema { + name: "mark", + attributes: &[], + }, + ElementSchema { + name: "marquee", + attributes: &[], + }, + ElementSchema { + name: "menu", + attributes: &[], + }, + ElementSchema { + name: "meter", + attributes: &[], + }, + ElementSchema { + name: "nav", + attributes: &[], + }, + ElementSchema { + name: "ol", + attributes: &[], + }, + ElementSchema { + name: "p", + attributes: &[], + }, + ElementSchema { + name: "pre", + attributes: &[], + }, + ElementSchema { + name: "progress", + attributes: &[], + }, + ElementSchema { + name: "ruby", + attributes: &[], + }, + ElementSchema { + name: "table", + attributes: &[], + }, + ElementSchema { + name: "time", + attributes: &[], + }, + ElementSchema { + name: "tr", + attributes: &[], + }, + ElementSchema { + name: "ul", + attributes: &[], + }, +]; + diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index 57b6d42e83367..7e87ecabb5cb6 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -27,6 +27,7 @@ doctest = true [dependencies] oxc_allocator = { workspace = true, features = ["fixed_size"] } +oxc_aria_query = { workspace = true } oxc_ast = { workspace = true } oxc_ast_macros = { workspace = true } oxc_ast_visit = { workspace = true, features = ["serialize"] } diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 7df2e4d483570..3779bd8c19ede 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1715,6 +1715,12 @@ impl RuleRunner for crate::rules::jsx_a11y::no_redundant_roles::NoRedundantRoles const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } +impl RuleRunner for crate::rules::jsx_a11y::no_static_element_interactions::NoStaticElementInteractions { + const NODE_TYPES: Option<&AstTypesBitset> = + Some(&AstTypesBitset::from_types(&[AstType::JSXOpeningElement])); + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::jsx_a11y::prefer_tag_over_role::PreferTagOverRole { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[AstType::JSXOpeningElement])); diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 425501210deef..60703421b5b1b 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -531,6 +531,7 @@ pub(crate) mod jsx_a11y { pub mod no_distracting_elements; pub mod no_noninteractive_tabindex; pub mod no_redundant_roles; + pub mod no_static_element_interactions; pub mod prefer_tag_over_role; pub mod role_has_required_aria_props; pub mod role_supports_aria_props; @@ -934,6 +935,7 @@ oxc_macros::declare_all_lint_rules! { jsx_a11y::lang, jsx_a11y::media_has_caption, jsx_a11y::mouse_events_have_key_events, + jsx_a11y::no_static_element_interactions, jsx_a11y::no_noninteractive_tabindex, jsx_a11y::no_access_key, jsx_a11y::no_aria_hidden_on_focusable, diff --git a/crates/oxc_linter/src/rules/jsx_a11y/no_static_element_interactions.rs b/crates/oxc_linter/src/rules/jsx_a11y/no_static_element_interactions.rs new file mode 100644 index 0000000000000..ecb7caa709a22 --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/no_static_element_interactions.rs @@ -0,0 +1,273 @@ +use oxc_ast::{AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, Span}; + +use crate::{ + AstNode, + context::LintContext, + globals::HTML_TAG, + rule::Rule, + utils::{has_jsx_prop, get_prop_value, get_element_type, + is_hidden_from_screen_reader, is_presentation_role, + is_interactive_element, is_interactive_role, is_noninteractive_element, + is_noninteractive_role, is_abstract_role, is_nonliteral_property}, +}; + +fn no_static_element_interactions_diagnostic(span: Span) -> OxcDiagnostic { + // See for details + OxcDiagnostic::warn("Do not attach interactive event handlers to static elements without an interactive role.") + .with_help("Use a semantic interactive element (like + /// ``` + /// + /// ### Options + /// + /// #### `allowExpressionValues` + /// + /// `{ type: boolean, default: true }` + /// + /// Dynamic role values are allowed when `allowExpressionValues` is true: + /// ```jsx + ///
Go
+ /// ``` + /// + /// #### `handlers` + /// + /// ```ts + /// { + /// type: string[], + /// default: [ + /// "onClick", + /// "onMouseDown", + /// "onMouseUp", + /// "onKeyPress", + /// "onKeyDown", + /// "onKeyUp", + /// ] + /// } + /// ``` + /// + NoStaticElementInteractions, + jsx_a11y, + suspicious, +); + +impl Rule for NoStaticElementInteractions { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::JSXOpeningElement(jsx_opening_el) = node.kind() else { + return; + }; + + let element_type = get_element_type(ctx, jsx_opening_el); + if !HTML_TAG.contains(element_type.as_ref()) { + return; + } + + let has_interactive_props = self.0.handlers.iter().any(|prop| { + has_jsx_prop(jsx_opening_el, prop) + .is_some_and(|attr| get_prop_value(attr) + .is_some()) + }); + + if !has_interactive_props + || is_hidden_from_screen_reader(ctx, jsx_opening_el) + || is_presentation_role(jsx_opening_el) + { + return; + } + + if is_interactive_element(element_type.as_ref(), jsx_opening_el) + || is_interactive_role(jsx_opening_el) + || is_noninteractive_element(element_type.as_ref(), jsx_opening_el) + || is_noninteractive_role(element_type.as_ref(), jsx_opening_el) + || is_abstract_role(element_type.as_ref(), jsx_opening_el) + { + // This rule has no opinion about abstract roles. + return; + } + + if self.0.allow_expression_values && is_nonliteral_property(jsx_opening_el) { + return; + } + + ctx.diagnostic(no_static_element_interactions_diagnostic(jsx_opening_el.span)); + } + + fn from_configuration(value: serde_json::Value) -> Self { + let default = Self::default(); + + let Some(config) = value.get(0) else { + return default; + }; + + Self(Box::new(NoStaticElementInteractionsConfig { + handlers: config + .get("handlers") + .and_then(serde_json::Value::as_array) + .map_or(default.0.handlers, |v| { + v.iter().map(|v| CompactStr::new(v.as_str().unwrap())).collect() + }), + allow_expression_values: config + .get("allowExpressionValues") + .and_then(serde_json::Value::as_bool) + .unwrap_or(default.0.allow_expression_values), + })) + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("
{}} />;", None), + ("
{}} />;", None), + ( + "
{}} />;", + Some(serde_json::json!([{ "allowExpressionValues": true }])), + ), + ( + r#"
{}} />;"#, + Some(serde_json::json!([{ "allowExpressionValues": true }])), + ), + (r#"
{}} />;"#, None), + (r#"
{}} />;"#, None), + (r#"
{}} />;"#, None), + (r#" {}} />;"#, None), + (r#" {}} />;"#, None), + (r#"
{}} />;"#, None), + (r#"