diff --git a/apps/oxlint/src/snapshots/_-A all --print-config@oxlint.snap b/apps/oxlint/src/snapshots/_-A all --print-config@oxlint.snap index 2ba8dc774c924..afa0b27b74565 100644 --- a/apps/oxlint/src/snapshots/_-A all --print-config@oxlint.snap +++ b/apps/oxlint/src/snapshots/_-A all --print-config@oxlint.snap @@ -16,7 +16,8 @@ working directory: "settings": { "jsx-a11y": { "polymorphicPropName": null, - "components": {} + "components": {}, + "attributes": {} }, "next": { "rootDir": [] diff --git a/apps/oxlint/src/snapshots/_-c fixtures__print_config__ban_rules__eslintrc.json -A all -D eqeqeq --print-config@oxlint.snap b/apps/oxlint/src/snapshots/_-c fixtures__print_config__ban_rules__eslintrc.json -A all -D eqeqeq --print-config@oxlint.snap index 2ce422811914b..a19f93408dc7e 100644 --- a/apps/oxlint/src/snapshots/_-c fixtures__print_config__ban_rules__eslintrc.json -A all -D eqeqeq --print-config@oxlint.snap +++ b/apps/oxlint/src/snapshots/_-c fixtures__print_config__ban_rules__eslintrc.json -A all -D eqeqeq --print-config@oxlint.snap @@ -23,7 +23,8 @@ working directory: "settings": { "jsx-a11y": { "polymorphicPropName": null, - "components": {} + "components": {}, + "attributes": {} }, "next": { "rootDir": [] diff --git a/crates/oxc_linter/src/config/settings/jsx_a11y.rs b/crates/oxc_linter/src/config/settings/jsx_a11y.rs index e4f1203218484..1fae550f9232a 100644 --- a/crates/oxc_linter/src/config/settings/jsx_a11y.rs +++ b/crates/oxc_linter/src/config/settings/jsx_a11y.rs @@ -45,4 +45,23 @@ pub struct JSXA11yPluginSettings { /// ``` #[serde(default)] pub components: FxHashMap, + + /// Map of attribute names to their DOM equivalents. + /// This is useful for non-React frameworks that use different attribute names. + /// + /// Example: + /// + /// ```json + /// { + /// "settings": { + /// "jsx-a11y": { + /// "attributes": { + /// "for": ["htmlFor", "for"] + /// } + /// } + /// } + /// } + /// ``` + #[serde(default)] + pub attributes: FxHashMap>, } diff --git a/crates/oxc_linter/src/config/settings/mod.rs b/crates/oxc_linter/src/config/settings/mod.rs index e32c7d75822af..b986b479382f2 100644 --- a/crates/oxc_linter/src/config/settings/mod.rs +++ b/crates/oxc_linter/src/config/settings/mod.rs @@ -125,5 +125,42 @@ mod test { let settings = OxlintSettings::default(); assert!(settings.jsx_a11y.polymorphic_prop_name.is_none()); assert!(settings.jsx_a11y.components.is_empty()); + assert!(settings.jsx_a11y.attributes.is_empty()); + } + + #[test] + fn test_parse_jsx_a11y_attributes() { + let settings = OxlintSettings::deserialize(&serde_json::json!({ + "jsx-a11y": { + "attributes": { + "for": ["htmlFor", "for"], + "class": ["className"] + } + } + })) + .unwrap(); + + let for_attrs = &settings.jsx_a11y.attributes["for"]; + assert_eq!(for_attrs.len(), 2); + assert_eq!(for_attrs[0], "htmlFor"); + assert_eq!(for_attrs[1], "for"); + + let class_attrs = &settings.jsx_a11y.attributes["class"]; + assert_eq!(class_attrs.len(), 1); + assert_eq!(class_attrs[0], "className"); + + assert_eq!(settings.jsx_a11y.attributes.get("nonexistent"), None); + } + + #[test] + fn test_parse_jsx_a11y_attributes_empty() { + let settings = OxlintSettings::deserialize(&serde_json::json!({ + "jsx-a11y": { + "attributes": {} + } + })) + .unwrap(); + + assert!(settings.jsx_a11y.attributes.is_empty()); } } diff --git a/crates/oxc_linter/src/rules/jsx_a11y/label_has_associated_control.rs b/crates/oxc_linter/src/rules/jsx_a11y/label_has_associated_control.rs index 4e043d9f2a043..c1d0cb600a92b 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/label_has_associated_control.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/label_has_associated_control.rs @@ -215,7 +215,14 @@ impl Rule for LabelHasAssociatedControl { return; } - let has_html_for = has_jsx_prop(&element.opening_element, "htmlFor").is_some(); + let has_html_for = if let Some(attributes) = ctx.settings().jsx_a11y.attributes.get("for") { + attributes + .iter() + .any(|attr| has_jsx_prop(&element.opening_element, attr.as_str()).is_some()) + } else { + has_jsx_prop(&element.opening_element, "htmlFor").is_some() + }; + let has_control = self.has_nested_control(element, ctx); if !self.has_accessible_label(element, ctx) { @@ -406,6 +413,18 @@ fn test() { }) } + fn attributes_settings() -> serde_json::Value { + serde_json::json!({ + "settings": { + "jsx-a11y": { + "attributes": { + "for": ["htmlFor", "for"] + } + } + } + }) + } + let pass = vec![ ( r#""#, @@ -934,6 +953,32 @@ fn test() { None, None, ), + // Test for 'for' attribute with attributes setting + ( + r#""#, + Some(serde_json::json!([{ "assert": "htmlFor" }])), + Some(attributes_settings()), + ), + ( + r#"