1use unicode_id_start::{is_id_continue_unicode, is_id_start_unicode};
2
3use crate::line_terminator::{CR, LF, LS, PS};
4
5pub const EOF: char = '\0';
6
7pub const ZWNJ: char = '\u{200c}';
12
13pub const ZWJ: char = '\u{200d}';
16
17pub const ZWNBSP: char = '\u{feff}';
20
21pub const TAB: char = '\u{9}';
24
25pub const VT: char = '\u{b}';
27
28pub const FF: char = '\u{c}';
30
31pub const SP: char = '\u{20}';
33
34pub const NBSP: char = '\u{a0}';
36
37const NEL: char = '\u{85}';
39
40const OGHAM_SPACE_MARK: char = '\u{1680}';
41
42const EN_QUAD: char = '\u{2000}';
43
44const ZWSP: char = '\u{200b}';
46
47const NNBSP: char = '\u{202f}';
49
50const MMSP: char = '\u{205f}';
52
53const IDEOGRAPHIC_SPACE: char = '\u{3000}';
54
55fn is_unicode_space_separator(c: char) -> bool {
56 c.is_whitespace() && !matches!(c, TAB | LF | VT | FF | CR | NEL | LS | PS)
60}
61
62pub fn is_white_space(c: char) -> bool {
63 matches!(c, TAB | VT | FF | ZWNBSP) || is_unicode_space_separator(c)
64}
65
66#[rustfmt::skip]
68pub fn is_irregular_whitespace(c: char) -> bool {
69 matches!(c,
70 VT | FF | NBSP | ZWNBSP | NEL | OGHAM_SPACE_MARK
71 | EN_QUAD..=ZWSP | NNBSP | MMSP | IDEOGRAPHIC_SPACE
72 )
73}
74
75pub fn is_white_space_single_line(c: char) -> bool {
77 matches!(c, SP | TAB) || is_irregular_whitespace(c)
80}
81
82const ID_START: u8 = 1;
83const ID_CONTINUE: u8 = 2;
84
85#[repr(C, align(64))]
86pub struct Align64<T>(pub(crate) T);
87
88#[rustfmt::skip]
92pub static ASCII_ID_FLAGS: Align64<[u8; 128]> = Align64([
930, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 3, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, ]);
103
104#[inline]
106pub fn is_identifier_start(c: char) -> bool {
107 if c.is_ascii() {
108 return is_identifier_start_ascii(c);
109 }
110 is_identifier_start_unicode(c)
111}
112
113#[inline]
114pub fn is_identifier_start_ascii(c: char) -> bool {
115 ASCII_ID_FLAGS.0[c as usize] & ID_START != 0
116}
117
118#[inline]
119pub fn is_identifier_start_unicode(c: char) -> bool {
120 is_id_start_unicode(c)
121}
122
123#[inline]
126pub fn is_identifier_part(c: char) -> bool {
127 if c.is_ascii() {
128 return is_identifier_part_ascii(c);
129 }
130 is_identifier_part_unicode(c)
131}
132
133#[inline]
134pub fn is_identifier_part_ascii(c: char) -> bool {
135 ASCII_ID_FLAGS.0[c as usize] & ID_CONTINUE != 0
136}
137
138#[inline]
139pub fn is_identifier_part_unicode(c: char) -> bool {
140 is_id_continue_unicode(c) || c == ZWNJ || c == ZWJ
141}
142
143const KATAKANA_MIDDLE_DOT: char = '・';
145const HALFWIDTH_KATAKANA_MIDDLE_DOT: char = '・';
147
148pub fn is_identifier_name(name: &str) -> bool {
150 is_identifier_name_impl::<false>(name)
151}
152
153pub fn is_identifier_name_patched(name: &str) -> bool {
160 is_identifier_name_impl::<true>(name)
161}
162
163fn is_identifier_name_impl<const PATCHED: bool>(name: &str) -> bool {
164 let bytes = name.as_bytes();
172 let Some(&first_byte) = bytes.first() else { return false };
173
174 let mut chars = if first_byte.is_ascii() {
175 if ASCII_ID_FLAGS.0[first_byte as usize] & ID_START == 0 {
177 return false;
178 }
179
180 let mut index = 1;
181 'outer: loop {
182 let bytes_remaining = bytes.len() - index;
184 if bytes_remaining >= 8 {
185 #[expect(clippy::cast_ptr_alignment)]
189 let next8_as_u64 = unsafe {
190 let ptr = bytes.as_ptr().add(index).cast::<u64>();
191 ptr.read_unaligned()
192 };
193 let high_bits = next8_as_u64 & 0x8080_8080_8080_8080;
194 if high_bits != 0 {
195 break;
197 }
198
199 let next8 = next8_as_u64.to_ne_bytes();
200 for b in next8 {
201 if ASCII_ID_FLAGS.0[b as usize] & ID_CONTINUE == 0 {
202 return false;
203 }
204 }
205
206 index += 8;
207 } else if bytes_remaining >= 4 {
208 #[expect(clippy::cast_ptr_alignment)]
212 let next4_as_u32 = unsafe {
213 let ptr = bytes.as_ptr().add(index).cast::<u32>();
214 ptr.read_unaligned()
215 };
216 let high_bits = next4_as_u32 & 0x8080_8080;
217 if high_bits != 0 {
218 break;
220 }
221
222 let next4 = next4_as_u32.to_ne_bytes();
223 for b in next4 {
224 if ASCII_ID_FLAGS.0[b as usize] & ID_CONTINUE == 0 {
225 return false;
226 }
227 }
228
229 index += 4;
230 } else {
231 loop {
232 let Some(&b) = bytes.get(index) else {
233 return true;
235 };
236
237 if b.is_ascii() {
238 if ASCII_ID_FLAGS.0[b as usize] & ID_CONTINUE == 0 {
239 return false;
240 }
241 } else {
242 break 'outer;
244 }
245
246 index += 1;
247 }
248 }
249 }
250
251 name[index..].chars()
253 } else {
254 let mut chars = name.chars();
257 let first_char = chars.next().unwrap();
258 if !is_identifier_start_unicode(first_char) {
259 return false;
260 }
261 chars
263 };
264
265 if PATCHED {
267 chars.all(|c| {
268 is_identifier_part(c) && c != KATAKANA_MIDDLE_DOT && c != HALFWIDTH_KATAKANA_MIDDLE_DOT
269 })
270 } else {
271 chars.all(is_identifier_part)
272 }
273}
274
275#[test]
276fn is_identifier_name_true() {
277 let cases = [
278 "a",
280 "z",
281 "A",
282 "Z",
283 "_",
284 "$",
285 "µ", "ख", "𐀀", "az",
291 "AZ",
292 "_a",
293 "$Z",
294 "a0",
295 "A9",
296 "_0",
297 "$9",
298 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$",
299 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_$",
300 "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$",
301 "$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_",
302 "µख𐀀",
304 "AµBखC𐀀D",
306 "µAखB𐀀",
308 ];
309
310 for str in cases {
311 assert!(is_identifier_name(str));
312 }
313}
314
315#[test]
316fn is_identifier_name_false() {
317 let cases = [
318 "",
320 "0",
322 "9",
323 "-",
324 "~",
325 "+",
326 "£", "৸", "𐄬", "0a",
332 "9a",
333 "-a",
334 "+a",
335 "a-Z",
336 "A+z",
337 "a-",
338 "a+",
339 "£৸𐄬",
341 "A£",
343 "A৸",
344 "A𐄬",
345 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$abc£",
346 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$abc৸",
347 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$abc𐄬",
348 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$abc£abcdefghijklmnopqrstuvwxyz",
349 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$abc৸abcdefghijklmnopqrstuvwxyz",
350 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$abc𐄬abcdefghijklmnopqrstuvwxyz",
351 "£A",
353 "৸A",
354 "𐄬A",
355 ];
356
357 for str in cases {
358 assert!(!is_identifier_name(str));
359 }
360}
361
362#[test]
363fn is_identifier_name_patched_rejects_katakana_dots() {
364 assert!(is_identifier_name("x\u{30FB}"));
368 assert!(!is_identifier_name_patched("x\u{30FB}"));
369 assert!(is_identifier_name("x\u{FF65}"));
371 assert!(!is_identifier_name_patched("x\u{FF65}"));
372 assert!(!is_identifier_name("\u{30FB}"));
374 assert!(!is_identifier_name_patched("\u{30FB}"));
375 assert!(is_identifier_name_patched("foo"));
377 assert!(is_identifier_name_patched("_bar"));
378 assert!(is_identifier_name_patched("$baz"));
379 assert!(is_identifier_name_patched("µ"));
380 assert!(!is_identifier_name_patched(""));
382}