@@ -5,19 +5,92 @@ pub struct VariableUsages;
55
66impl SearchCondition for VariableUsages {
77 fn matches ( & self , prop : & Property ) -> bool {
8- prop . value . raw . contains ( "var(" ) || has_dashed_ident ( prop. value . raw . as_ref ( ) )
8+ contains_variable_reference ( prop. value . raw . as_ref ( ) )
99 }
1010}
1111
12- fn has_dashed_ident ( value : & str ) -> bool {
12+ fn contains_variable_reference ( value : & str ) -> bool {
1313 let bytes = value. as_bytes ( ) ;
14- ( 0 ..bytes. len ( ) . saturating_sub ( 2 ) ) . any ( |i| {
15- bytes[ i] == b'-'
16- && bytes[ i + 1 ] == b'-'
17- && ( bytes[ i + 2 ] . is_ascii_alphanumeric ( )
18- || bytes[ i + 2 ] == b'_'
19- || bytes[ i + 2 ] == b'-' )
20- } )
14+ let len = bytes. len ( ) ;
15+ let mut i = 0 ;
16+ while i < len {
17+ match bytes[ i] {
18+ b'"' | b'\'' => i = end_of_string ( bytes, i) ,
19+ b'/' if i + 1 < len && bytes[ i + 1 ] == b'*' => i = end_of_comment ( bytes, i) ,
20+ b'u' | b'U' if at_token_boundary ( bytes, i) && matches_func_open ( bytes, i, b"url" ) => {
21+ i = end_of_url ( bytes, i + 4 ) ;
22+ }
23+ b'v' | b'V' if at_token_boundary ( bytes, i) && matches_func_open ( bytes, i, b"var" ) => {
24+ return true ;
25+ }
26+ b'-' if at_token_boundary ( bytes, i)
27+ && i + 2 < len
28+ && bytes[ i + 1 ] == b'-'
29+ && is_ident_continue ( bytes[ i + 2 ] ) =>
30+ {
31+ return true ;
32+ }
33+ _ => i += 1 ,
34+ }
35+ }
36+ false
37+ }
38+
39+ fn at_token_boundary ( bytes : & [ u8 ] , i : usize ) -> bool {
40+ i == 0 || !is_ident_continue ( bytes[ i - 1 ] )
41+ }
42+
43+ fn is_ident_continue ( b : u8 ) -> bool {
44+ b. is_ascii_alphanumeric ( ) || b == b'_' || b == b'-' || b >= 0x80
45+ }
46+
47+ fn matches_func_open ( bytes : & [ u8 ] , i : usize , name : & [ u8 ] ) -> bool {
48+ let end = i + name. len ( ) ;
49+ end < bytes. len ( ) && bytes[ i..end] . eq_ignore_ascii_case ( name) && bytes[ end] == b'('
50+ }
51+
52+ fn end_of_string ( bytes : & [ u8 ] , start : usize ) -> usize {
53+ let quote = bytes[ start] ;
54+ let mut i = start + 1 ;
55+ while i < bytes. len ( ) {
56+ match bytes[ i] {
57+ b'\\' if i + 1 < bytes. len ( ) => i += 2 ,
58+ b if b == quote => return i + 1 ,
59+ _ => i += 1 ,
60+ }
61+ }
62+ bytes. len ( )
63+ }
64+
65+ fn end_of_comment ( bytes : & [ u8 ] , start : usize ) -> usize {
66+ let mut i = start + 2 ;
67+ while i + 1 < bytes. len ( ) {
68+ if bytes[ i] == b'*' && bytes[ i + 1 ] == b'/' {
69+ return i + 2 ;
70+ }
71+ i += 1 ;
72+ }
73+ bytes. len ( )
74+ }
75+
76+ fn end_of_url ( bytes : & [ u8 ] , start : usize ) -> usize {
77+ let mut i = start;
78+ let mut paren = 1i32 ;
79+ while i < bytes. len ( ) && paren > 0 {
80+ match bytes[ i] {
81+ b'"' | b'\'' => i = end_of_string ( bytes, i) ,
82+ b'(' => {
83+ paren += 1 ;
84+ i += 1 ;
85+ }
86+ b')' => {
87+ paren -= 1 ;
88+ i += 1 ;
89+ }
90+ _ => i += 1 ,
91+ }
92+ }
93+ i
2194}
2295
2396#[ cfg( test) ]
@@ -62,4 +135,58 @@ mod tests {
62135 fn rejects_bare_double_dash ( ) {
63136 assert ! ( !matches_value( "--" ) ) ;
64137 }
138+
139+ #[ test]
140+ fn rejects_dashed_ident_inside_double_quoted_string ( ) {
141+ assert ! ( !matches_value( "\" --not-a-var\" " ) ) ;
142+ }
143+
144+ #[ test]
145+ fn rejects_dashed_ident_inside_single_quoted_string ( ) {
146+ assert ! ( !matches_value( "'--not-a-var'" ) ) ;
147+ }
148+
149+ #[ test]
150+ fn rejects_var_call_inside_string ( ) {
151+ assert ! ( !matches_value( "\" var(--x)\" " ) ) ;
152+ }
153+
154+ #[ test]
155+ fn rejects_dashed_ident_inside_quoted_url ( ) {
156+ assert ! ( !matches_value( "url(\" file--name.png\" )" ) ) ;
157+ }
158+
159+ #[ test]
160+ fn rejects_dashed_ident_inside_unquoted_url ( ) {
161+ assert ! ( !matches_value( "url(file--name.png)" ) ) ;
162+ }
163+
164+ #[ test]
165+ fn rejects_var_call_inside_unquoted_url ( ) {
166+ assert ! ( !matches_value( "url(var(--x))" ) ) ;
167+ }
168+
169+ #[ test]
170+ fn rejects_dashed_ident_glued_to_preceding_ident ( ) {
171+ // `font--name` is a single identifier-like token, not a `--name` reference.
172+ assert ! ( !matches_value( "font--name" ) ) ;
173+ }
174+
175+ #[ test]
176+ fn matches_var_call_uppercase ( ) {
177+ assert ! ( matches_value( "VAR(--x)" ) ) ;
178+ assert ! ( matches_value( "Var(--x)" ) ) ;
179+ }
180+
181+ #[ test]
182+ fn matches_real_var_alongside_string_with_dashes ( ) {
183+ assert ! ( matches_value( "\" --decoy\" var(--real)" ) ) ;
184+ }
185+
186+ #[ test]
187+ fn rejects_dashed_ident_inside_block_comment ( ) {
188+ // Comments don't normally survive into the value (parser strips them),
189+ // but be defensive.
190+ assert ! ( !matches_value( "/* --decoy */ red" ) ) ;
191+ }
65192}
0 commit comments