Skip to content

Commit 27a3dd9

Browse files
committed
fix(linter): support jsx-a11y attributes setting in anchor-is-valid rule
1 parent ca1169f commit 27a3dd9

2 files changed

Lines changed: 87 additions & 6 deletions

File tree

crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ use crate::{
1717
utils::{get_element_type, has_jsx_prop_ignore_case},
1818
};
1919

20-
fn missing_href_attribute(span: Span) -> OxcDiagnostic {
20+
fn missing_href_attribute(span: Span, valid_attrs: &[CompactStr]) -> OxcDiagnostic {
21+
let help = if valid_attrs.len() == 1 {
22+
format!("Provide a `{}` for the `a` element.", valid_attrs[0])
23+
} else {
24+
let list = valid_attrs.iter().map(|a| format!("`{a}`")).collect::<Vec<_>>().join(", ");
25+
format!("Provide one of {list} for the `a` element.")
26+
};
2127
OxcDiagnostic::warn("Missing `href` attribute for the `a` element.")
22-
.with_help("Provide an `href` for the `a` element.")
28+
.with_help(help)
2329
.with_label(span)
2430
}
2531

@@ -142,7 +148,18 @@ impl Rule for AnchorIsValid {
142148
}
143149
// Don't eagerly get `span` here, to avoid that work unless rule fails
144150
let get_span = || jsx_el.opening_element.name.span();
145-
if let Some(href_attr) = has_jsx_prop_ignore_case(&jsx_el.opening_element, "href") {
151+
// Check href or any configured alternative attribute names (e.g. `to` for router Link components)
152+
let href_names: Vec<CompactStr> = ctx
153+
.settings()
154+
.jsx_a11y
155+
.attributes
156+
.get("href")
157+
.cloned()
158+
.unwrap_or_else(|| vec![CompactStr::from("href")]);
159+
let href_attr = href_names
160+
.iter()
161+
.find_map(|n| has_jsx_prop_ignore_case(&jsx_el.opening_element, n.as_str()));
162+
if let Some(href_attr) = href_attr {
146163
let JSXAttributeItem::Attribute(attr) = href_attr else {
147164
return;
148165
};
@@ -172,7 +189,7 @@ impl Rule for AnchorIsValid {
172189
if has_spread_attr {
173190
return;
174191
}
175-
ctx.diagnostic(missing_href_attribute(get_span()));
192+
ctx.diagnostic(missing_href_attribute(get_span(), &href_names));
176193
}
177194
}
178195
}
@@ -315,6 +332,31 @@ fn test() {
315332
serde_json::json!({ "settings": { "jsx-a11y": { "components": { "Anchor": "a", "Link": "a" } } } }),
316333
),
317334
),
335+
// attributes settings: `to` is a valid alternative for `href` on Link components
336+
(
337+
r"<Link to='https://example.com' />",
338+
None,
339+
Some(serde_json::json!({
340+
"settings": {
341+
"jsx-a11y": {
342+
"components": { "Link": "a" },
343+
"attributes": { "href": ["href", "to"] }
344+
}
345+
}
346+
})),
347+
),
348+
(
349+
r"<Link to={dest} />",
350+
None,
351+
Some(serde_json::json!({
352+
"settings": {
353+
"jsx-a11y": {
354+
"components": { "Link": "a" },
355+
"attributes": { "href": ["href", "to"] }
356+
}
357+
}
358+
})),
359+
),
318360
// (r#"<a {...props} />"#, Some(serde_json::json!(specialLink))),
319361
// (r#"<a hrefLeft='foo' />"#, Some(serde_json::json!(specialLink))),
320362
// (r#"<a hrefLeft={foo} />"#, Some(serde_json::json!(specialLink))),
@@ -615,6 +657,31 @@ fn test() {
615657
serde_json::json!({ "settings": { "jsx-a11y": { "components": { "Anchor": "a", "Link": "a" } } } }),
616658
),
617659
),
660+
// attributes settings: Link without `to` or `href` should still fail
661+
(
662+
r"<Link />",
663+
None,
664+
Some(serde_json::json!({
665+
"settings": {
666+
"jsx-a11y": {
667+
"components": { "Link": "a" },
668+
"attributes": { "href": ["href", "to"] }
669+
}
670+
}
671+
})),
672+
),
673+
(
674+
r"<Link to='#' />",
675+
None,
676+
Some(serde_json::json!({
677+
"settings": {
678+
"jsx-a11y": {
679+
"components": { "Link": "a" },
680+
"attributes": { "href": ["href", "to"] }
681+
}
682+
}
683+
})),
684+
),
618685
// (r#"<a hrefLeft={undefined} />"#, Some(serde_json::json!(specialLink))),
619686
// (r#"<a hrefLeft={null} />"#, Some(serde_json::json!(specialLink))),
620687
// (r#"<a hrefLeft=' />;"#, Some(serde_json::json!(specialLink))),

crates/oxc_linter/src/snapshots/jsx_a11y_anchor_is_valid.snap

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ source: crates/oxc_linter/src/tester.rs
77
1<a />
88
· ─
99
╰────
10-
help: Provide an `href` for the `a` element.
10+
help: Provide a `href` for the `a` element.
1111

1212
eslint-plugin-jsx-a11y(anchor-is-valid): Use of incorrect `href` for the 'a' element.
1313
╭─[anchor_is_valid.tsx:1:2]
@@ -70,7 +70,7 @@ source: crates/oxc_linter/src/tester.rs
7070
1<a onClick={() => void 0} />
7171
· ─
7272
╰────
73-
help: Provide an `href` for the `a` element.
73+
help: Provide a `href` for the `a` element.
7474

7575
eslint-plugin-jsx-a11y(anchor-is-valid): The `a` element has `href` and `onClick`.
7676
╭─[anchor_is_valid.tsx:1:2]
@@ -99,3 +99,17 @@ source: crates/oxc_linter/src/tester.rs
9999
· ────
100100
╰────
101101
help: Use a `button` element instead of an `a` element.
102+
103+
eslint-plugin-jsx-a11y(anchor-is-valid): Missing `href` attribute for the `a` element.
104+
╭─[anchor_is_valid.tsx:1:2]
105+
1<Link />
106+
· ────
107+
╰────
108+
help: Provide one of `href`, `to` for the `a` element.
109+
110+
⚠ eslint-plugin-jsx-a11y(anchor-is-valid): Use of incorrect `href` for the 'a' element.
111+
╭─[anchor_is_valid.tsx:1:2]
112+
1 │ <Link to='#' />
113+
· ────
114+
╰────
115+
help: Provide a correct `href` for the `a` element.

0 commit comments

Comments
 (0)