1
+ use std:: ops:: Deref ;
2
+
1
3
use oxc_ast:: {
2
4
ast:: { JSXAttributeItem , JSXAttributeValue , JSXElementName , JSXExpression } ,
3
5
AstKind ,
4
6
} ;
5
7
use oxc_diagnostics:: OxcDiagnostic ;
6
8
use oxc_macros:: declare_oxc_lint;
7
- use oxc_span:: Span ;
9
+ use oxc_span:: { CompactStr , Span } ;
10
+ use serde_json:: Value ;
8
11
9
12
use crate :: {
10
13
context:: LintContext ,
@@ -35,8 +38,17 @@ fn cant_be_anchor(span0: Span) -> OxcDiagnostic {
35
38
pub struct AnchorIsValid ( Box < AnchorIsValidConfig > ) ;
36
39
37
40
#[ derive( Debug , Default , Clone ) ]
38
- struct AnchorIsValidConfig {
39
- valid_hrefs : Vec < String > ,
41
+ pub struct AnchorIsValidConfig {
42
+ /// Unique and sorted list of valid hrefs
43
+ valid_hrefs : Vec < CompactStr > ,
44
+ }
45
+
46
+ impl Deref for AnchorIsValid {
47
+ type Target = AnchorIsValidConfig ;
48
+
49
+ fn deref ( & self ) -> & Self :: Target {
50
+ & self . 0
51
+ }
40
52
}
41
53
42
54
declare_oxc_lint ! (
@@ -109,11 +121,10 @@ declare_oxc_lint!(
109
121
110
122
impl Rule for AnchorIsValid {
111
123
fn from_configuration ( value : serde_json:: Value ) -> Self {
112
- let valid_hrefs =
113
- value. get ( "validHrefs" ) . and_then ( |v| v. as_array ( ) ) . map_or_else ( Vec :: new, |array| {
114
- array. iter ( ) . filter_map ( |v| v. as_str ( ) . map ( String :: from) ) . collect :: < Vec < String > > ( )
115
- } ) ;
116
- Self ( Box :: new ( AnchorIsValidConfig { valid_hrefs } ) )
124
+ let Some ( valid_hrefs) = value. get ( "validHrefs" ) . and_then ( Value :: as_array) else {
125
+ return Self :: default ( ) ;
126
+ } ;
127
+ Self ( Box :: new ( valid_hrefs. iter ( ) . collect ( ) ) )
117
128
}
118
129
119
130
fn run < ' a > ( & self , node : & AstNode < ' a > , ctx : & LintContext < ' a > ) {
@@ -124,76 +135,82 @@ impl Rule for AnchorIsValid {
124
135
let Some ( name) = & get_element_type ( ctx, & jsx_el. opening_element ) else {
125
136
return ;
126
137
} ;
127
- if name == "a" {
128
- if let Option :: Some ( herf_attr) =
129
- has_jsx_prop_ignore_case ( & jsx_el. opening_element , "href" )
130
- {
131
- // Check if the 'a' element has a correct href attribute
132
- match herf_attr {
133
- JSXAttributeItem :: Attribute ( attr) => match & attr. value {
134
- Some ( value) => {
135
- let is_empty = check_value_is_empty ( value, & self . 0 . valid_hrefs ) ;
136
- if is_empty {
137
- if has_jsx_prop_ignore_case ( & jsx_el. opening_element , "onclick" )
138
- . is_some ( )
139
- {
140
- ctx. diagnostic ( cant_be_anchor ( ident. span ) ) ;
141
- return ;
142
- }
143
- ctx. diagnostic ( incorrect_href ( ident. span ) ) ;
144
- return ;
145
- }
146
- }
147
- None => {
148
- ctx. diagnostic ( incorrect_href ( ident. span ) ) ;
149
- return ;
150
- }
151
- } ,
152
- JSXAttributeItem :: SpreadAttribute ( _) => {
153
- // pass
154
- return ;
155
- }
156
- }
138
+ if name != "a" {
139
+ return ;
140
+ } ;
141
+ if let Option :: Some ( href_attr) =
142
+ has_jsx_prop_ignore_case ( & jsx_el. opening_element , "href" )
143
+ {
144
+ let JSXAttributeItem :: Attribute ( attr) = href_attr else {
157
145
return ;
158
- }
159
- // Exclude '<a {...props} />' case
160
- let has_spreed_attr =
161
- jsx_el. opening_element . attributes . iter ( ) . any ( |attr| match attr {
162
- JSXAttributeItem :: SpreadAttribute ( _) => true ,
163
- JSXAttributeItem :: Attribute ( _) => false ,
164
- } ) ;
165
- if has_spreed_attr {
146
+ } ;
147
+
148
+ // Check if the 'a' element has a correct href attribute
149
+ let Some ( value) = attr. value . as_ref ( ) else {
150
+ ctx. diagnostic ( incorrect_href ( ident. span ) ) ;
151
+ return ;
152
+ } ;
153
+
154
+ let is_empty = self . check_value_is_empty ( value) ;
155
+ if is_empty {
156
+ if has_jsx_prop_ignore_case ( & jsx_el. opening_element , "onclick" ) . is_some ( ) {
157
+ ctx. diagnostic ( cant_be_anchor ( ident. span ) ) ;
158
+ return ;
159
+ }
160
+ ctx. diagnostic ( incorrect_href ( ident. span ) ) ;
166
161
return ;
167
162
}
168
- ctx . diagnostic ( missing_href_attribute ( ident . span ) ) ;
163
+ return ;
169
164
}
165
+ // Exclude '<a {...props} />' case
166
+ let has_spread_attr = jsx_el. opening_element . attributes . iter ( ) . any ( |attr| match attr {
167
+ JSXAttributeItem :: SpreadAttribute ( _) => true ,
168
+ JSXAttributeItem :: Attribute ( _) => false ,
169
+ } ) ;
170
+ if has_spread_attr {
171
+ return ;
172
+ }
173
+ ctx. diagnostic ( missing_href_attribute ( ident. span ) ) ;
170
174
}
171
175
}
172
176
}
173
177
174
- fn check_value_is_empty ( value : & JSXAttributeValue , valid_hrefs : & [ String ] ) -> bool {
175
- match value {
176
- JSXAttributeValue :: Element ( _) => false ,
177
- JSXAttributeValue :: StringLiteral ( str_lit) => {
178
- let href_value = str_lit. value . to_string ( ) ; // Assuming Atom implements ToString
179
- href_value. is_empty ( )
180
- || href_value == "#"
181
- || href_value == "javascript:void(0)"
182
- || !valid_hrefs. contains ( & href_value)
178
+ impl AnchorIsValid {
179
+ fn check_value_is_empty ( & self , value : & JSXAttributeValue ) -> bool {
180
+ match value {
181
+ JSXAttributeValue :: Element ( _) => false ,
182
+ JSXAttributeValue :: StringLiteral ( str_lit) => self . is_invalid_href ( & str_lit. value ) ,
183
+ JSXAttributeValue :: ExpressionContainer ( exp) => match & exp. expression {
184
+ JSXExpression :: Identifier ( ident) => ident. name == "undefined" ,
185
+ JSXExpression :: NullLiteral ( _) => true ,
186
+ JSXExpression :: StringLiteral ( str_lit) => self . is_invalid_href ( & str_lit. value ) ,
187
+ _ => false ,
188
+ } ,
189
+ JSXAttributeValue :: Fragment ( _) => true ,
183
190
}
184
- JSXAttributeValue :: ExpressionContainer ( exp) => match & exp. expression {
185
- JSXExpression :: Identifier ( ident) => ident. name == "undefined" ,
186
- JSXExpression :: NullLiteral ( _) => true ,
187
- JSXExpression :: StringLiteral ( str_lit) => {
188
- let href_value = str_lit. value . to_string ( ) ;
189
- href_value. is_empty ( )
190
- || href_value == "#"
191
- || href_value == "javascript:void(0)"
192
- || !valid_hrefs. contains ( & href_value)
193
- }
194
- _ => false ,
195
- } ,
196
- JSXAttributeValue :: Fragment ( _) => true ,
191
+ }
192
+ }
193
+
194
+ impl AnchorIsValidConfig {
195
+ fn new ( mut valid_hrefs : Vec < CompactStr > ) -> Self {
196
+ valid_hrefs. sort_unstable ( ) ;
197
+ valid_hrefs. dedup ( ) ;
198
+ Self { valid_hrefs }
199
+ }
200
+
201
+ fn is_invalid_href ( & self , href : & str ) -> bool {
202
+ href. is_empty ( ) || href == "#" || href == "javascript:void(0)" || !self . contains ( href)
203
+ }
204
+
205
+ fn contains ( & self , href : & str ) -> bool {
206
+ self . valid_hrefs . binary_search_by ( |valid_href| valid_href. as_str ( ) . cmp ( href) ) . is_ok ( )
207
+ }
208
+ }
209
+
210
+ impl < ' v > FromIterator < & ' v Value > for AnchorIsValidConfig {
211
+ fn from_iter < T : IntoIterator < Item = & ' v Value > > ( iter : T ) -> Self {
212
+ let hrefs = iter. into_iter ( ) . filter_map ( Value :: as_str) . map ( CompactStr :: from) . collect ( ) ;
213
+ Self :: new ( hrefs)
197
214
}
198
215
}
199
216
0 commit comments