1
- use oxc_ast:: AstKind ;
1
+ use oxc_ast:: {
2
+ ast:: { JSXAttributeItem , JSXChild , JSXElement } ,
3
+ AstKind ,
4
+ } ;
2
5
use oxc_diagnostics:: OxcDiagnostic ;
3
6
use oxc_macros:: declare_oxc_lint;
4
7
use oxc_span:: Span ;
5
8
6
9
use crate :: {
7
10
context:: LintContext ,
11
+ fixer:: { Fix , RuleFix } ,
8
12
rule:: Rule ,
9
13
utils:: {
10
14
get_element_type, has_jsx_prop_ignore_case, is_hidden_from_screen_reader,
@@ -19,12 +23,6 @@ fn missing_content(span0: Span) -> OxcDiagnostic {
19
23
. with_label ( span0)
20
24
}
21
25
22
- fn remove_aria_hidden ( span0 : Span ) -> OxcDiagnostic {
23
- OxcDiagnostic :: warn ( "Missing accessible content when using `a` elements." )
24
- . with_help ( "Remove the `aria-hidden` attribute to allow the anchor element and its content visible to assistive technologies." )
25
- . with_label ( span0)
26
- }
27
-
28
26
#[ derive( Debug , Default , Clone ) ]
29
27
pub struct AnchorHasContent ;
30
28
@@ -59,7 +57,8 @@ declare_oxc_lint!(
59
57
/// ```
60
58
///
61
59
AnchorHasContent ,
62
- correctness
60
+ correctness,
61
+ conditional_suggestion
63
62
) ;
64
63
65
64
impl Rule for AnchorHasContent {
@@ -70,7 +69,6 @@ impl Rule for AnchorHasContent {
70
69
} ;
71
70
if name == "a" {
72
71
if is_hidden_from_screen_reader ( ctx, & jsx_el. opening_element ) {
73
- ctx. diagnostic ( remove_aria_hidden ( jsx_el. span ) ) ;
74
72
return ;
75
73
}
76
74
@@ -84,12 +82,43 @@ impl Rule for AnchorHasContent {
84
82
} ;
85
83
}
86
84
87
- ctx. diagnostic ( missing_content ( jsx_el. span ) ) ;
85
+ let diagnostic = missing_content ( jsx_el. span ) ;
86
+ if jsx_el. children . len ( ) == 1 {
87
+ let child = & jsx_el. children [ 0 ] ;
88
+ if let JSXChild :: Element ( child) = child {
89
+ ctx. diagnostic_with_suggestion ( diagnostic, |_fixer| {
90
+ remove_hidden_attributes ( child)
91
+ } ) ;
92
+ return ;
93
+ }
94
+ }
95
+
96
+ ctx. diagnostic ( diagnostic) ;
88
97
}
89
98
}
90
99
}
91
100
}
92
101
102
+ fn remove_hidden_attributes < ' a > ( element : & JSXElement < ' a > ) -> RuleFix < ' a > {
103
+ element
104
+ . opening_element
105
+ . attributes
106
+ . iter ( )
107
+ . filter_map ( JSXAttributeItem :: as_attribute)
108
+ . filter_map ( |attr| {
109
+ attr. name . as_identifier ( ) . and_then ( |name| {
110
+ if name. name . eq_ignore_ascii_case ( "aria-hidden" )
111
+ || name. name . eq_ignore_ascii_case ( "hidden" )
112
+ {
113
+ Some ( Fix :: delete ( attr. span ) )
114
+ } else {
115
+ None
116
+ }
117
+ } )
118
+ } )
119
+ . collect ( )
120
+ }
121
+
93
122
#[ test]
94
123
fn test ( ) {
95
124
use crate :: tester:: Tester ;
@@ -114,12 +143,28 @@ fn test() {
114
143
( r"<a title={title} />" , None , None ) ,
115
144
( r"<a aria-label={ariaLabel} />" , None , None ) ,
116
145
( r"<a title={title} aria-label={ariaLabel} />" , None , None ) ,
146
+ ( r#"<a><Bar aria-hidden="false" /></a>"# , None , None ) ,
147
+ // anchors can be hidden
148
+ ( r"<a aria-hidden>Foo</a>" , None , None ) ,
149
+ ( r#"<a aria-hidden="true">Foo</a>"# , None , None ) ,
150
+ ( r"<a hidden>Foo</a>" , None , None ) ,
151
+ ( r"<a aria-hidden><span aria-hidden>Foo</span></a>" , None , None ) ,
152
+ ( r#"<a hidden="true">Foo</a>"# , None , None ) ,
153
+ ( r#"<a hidden="">Foo</a>"# , None , None ) ,
154
+ // TODO: should these be failing?
155
+ ( r"<a><div hidden /></a>" , None , None ) ,
156
+ ( r"<a><Bar hidden /></a>" , None , None ) ,
157
+ ( r#"<a><Bar hidden="" /></a>"# , None , None ) ,
158
+ ( r#"<a><Bar hidden="until-hidden" /></a>"# , None , None ) ,
117
159
] ;
118
160
119
161
let fail = vec ! [
120
162
( r"<a />" , None , None ) ,
121
163
( r"<a><Bar aria-hidden /></a>" , None , None ) ,
164
+ ( r#"<a><Bar aria-hidden="true" /></a>"# , None , None ) ,
165
+ ( r#"<a><input type="hidden" /></a>"# , None , None ) ,
122
166
( r"<a>{undefined}</a>" , None , None ) ,
167
+ ( r"<a>{null}</a>" , None , None ) ,
123
168
(
124
169
r"<Link />" ,
125
170
None ,
@@ -129,5 +174,15 @@ fn test() {
129
174
) ,
130
175
] ;
131
176
132
- Tester :: new ( AnchorHasContent :: NAME , pass, fail) . test_and_snapshot ( ) ;
177
+ let fix = vec ! [
178
+ ( r"<a><Bar aria-hidden /></a>" , "<a><Bar /></a>" ) ,
179
+ ( r"<a><Bar aria-hidden>Can't see me</Bar></a>" , r"<a><Bar >Can't see me</Bar></a>" ) ,
180
+ ( r"<a><Bar aria-hidden={true}>Can't see me</Bar></a>" , r"<a><Bar >Can't see me</Bar></a>" ) ,
181
+ (
182
+ r#"<a><Bar aria-hidden="true">Can't see me</Bar></a>"# ,
183
+ r"<a><Bar >Can't see me</Bar></a>" ,
184
+ ) ,
185
+ ] ;
186
+
187
+ Tester :: new ( AnchorHasContent :: NAME , pass, fail) . expect_fix ( fix) . test_and_snapshot ( ) ;
133
188
}
0 commit comments