@@ -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))),
0 commit comments