1use std::borrow::Cow;
2
3use crate::string::CharProvider;
4
5use super::common::Range;
6use super::errors::*;
7use super::tokens::Token;
8
9pub struct Scanner<'a> {
11 byte_index: usize,
12 token_start: usize,
13 bytes: &'a [u8],
14 current_token: Option<Token<'a>>,
15 file_text: &'a str,
16 allow_single_quoted_strings: bool,
17 allow_hexadecimal_numbers: bool,
18 allow_unary_plus_numbers: bool,
19 allow_bare_decimal_point_numbers: bool,
20 allow_non_finite_numbers: bool,
21 allow_extended_string_escapes: bool,
22}
23
24#[derive(Debug)]
26pub struct ScannerOptions {
27 pub allow_single_quoted_strings: bool,
29 pub allow_hexadecimal_numbers: bool,
31 pub allow_unary_plus_numbers: bool,
33 pub allow_bare_decimal_point_numbers: bool,
35 pub allow_non_finite_numbers: bool,
37 pub allow_extended_string_escapes: bool,
39}
40
41impl Default for ScannerOptions {
42 fn default() -> Self {
43 Self {
44 allow_single_quoted_strings: true,
45 allow_hexadecimal_numbers: true,
46 allow_unary_plus_numbers: true,
47 allow_bare_decimal_point_numbers: true,
48 allow_non_finite_numbers: true,
49 allow_extended_string_escapes: true,
50 }
51 }
52}
53
54impl<'a> Scanner<'a> {
55 pub fn new(file_text: &'a str, options: &ScannerOptions) -> Scanner<'a> {
57 Scanner {
58 byte_index: 0,
59 token_start: 0,
60 bytes: file_text.as_bytes(),
61 current_token: None,
62 file_text,
63 allow_single_quoted_strings: options.allow_single_quoted_strings,
64 allow_hexadecimal_numbers: options.allow_hexadecimal_numbers,
65 allow_unary_plus_numbers: options.allow_unary_plus_numbers,
66 allow_bare_decimal_point_numbers: options.allow_bare_decimal_point_numbers,
67 allow_non_finite_numbers: options.allow_non_finite_numbers,
68 allow_extended_string_escapes: options.allow_extended_string_escapes,
69 }
70 }
71
72 pub fn file_text(&self) -> &str {
73 self.file_text
74 }
75
76 pub fn scan(&mut self) -> Result<Option<Token<'a>>, ParseError> {
78 self.skip_whitespace();
79 self.token_start = self.byte_index;
80 if let Some(&b) = self.bytes.get(self.byte_index) {
81 let token_result = match b {
82 b'{' => {
83 self.byte_index += 1;
84 Ok(Token::OpenBrace)
85 }
86 b'}' => {
87 self.byte_index += 1;
88 Ok(Token::CloseBrace)
89 }
90 b'[' => {
91 self.byte_index += 1;
92 Ok(Token::OpenBracket)
93 }
94 b']' => {
95 self.byte_index += 1;
96 Ok(Token::CloseBracket)
97 }
98 b',' => {
99 self.byte_index += 1;
100 Ok(Token::Comma)
101 }
102 b':' => {
103 self.byte_index += 1;
104 Ok(Token::Colon)
105 }
106 b'\'' => {
107 if self.allow_single_quoted_strings {
108 self.parse_string()
109 } else {
110 Err(self.create_error_for_current_token(ParseErrorKind::SingleQuotedStringsNotAllowed))
111 }
112 }
113 b'"' => self.parse_string(),
114 b'/' => match self.bytes.get(self.byte_index + 1) {
115 Some(b'/') => Ok(self.parse_comment_line()),
116 Some(b'*') => self.parse_comment_block(),
117 _ => Err(self.create_error_for_current_token(ParseErrorKind::UnexpectedToken)),
118 },
119 b'-' | b'+' | b'0'..=b'9' => self.parse_number(),
120 b'.' if matches!(self.bytes.get(self.byte_index + 1), Some(b'0'..=b'9')) => self.parse_number(),
121 b'I' if self.allow_non_finite_numbers && self.try_move_whole_word("Infinity") => Ok(Token::Number("Infinity")),
122 b'N' if self.allow_non_finite_numbers && self.try_move_whole_word("NaN") => Ok(Token::Number("NaN")),
123 b't' if self.try_move_word("true") => Ok(Token::Boolean(true)),
124 b'f' if self.try_move_word("false") => Ok(Token::Boolean(false)),
125 b'n' if self.try_move_word("null") => Ok(Token::Null),
126 _ => self.parse_word(),
127 };
128 match token_result {
129 Ok(token) => {
130 self.current_token = Some(token.clone());
131 Ok(Some(token))
132 }
133 Err(err) => Err(err),
134 }
135 } else {
136 self.current_token = None;
137 Ok(None)
138 }
139 }
140
141 pub fn token_start(&self) -> usize {
143 self.token_start
144 }
145
146 pub fn token_end(&self) -> usize {
148 self.byte_index
149 }
150
151 pub fn token(&self) -> Option<Token<'a>> {
153 self.current_token.as_ref().map(|x| x.to_owned())
154 }
155
156 pub(super) fn create_error_for_current_token(&self, kind: ParseErrorKind) -> ParseError {
157 let end = if self.byte_index > self.token_start {
158 self.byte_index
160 } else if let Some(c) = self.file_text[self.byte_index..].chars().next() {
161 self.byte_index + c.len_utf8()
163 } else {
164 self.file_text.len()
165 };
166 let range = Range {
167 start: self.token_start,
168 end,
169 };
170 self.create_error_for_range(range, kind)
171 }
172
173 pub(super) fn create_error_for_current_char(&self, kind: ParseErrorKind) -> ParseError {
174 self.create_error_for_start(self.byte_index, kind)
175 }
176
177 pub(super) fn create_error_for_start(&self, start: usize, kind: ParseErrorKind) -> ParseError {
178 let range = Range {
179 start,
180 end: if let Some(c) = self.file_text[self.byte_index..].chars().next() {
181 self.byte_index + c.len_utf8()
182 } else {
183 self.file_text.len()
184 },
185 };
186 self.create_error_for_range(range, kind)
187 }
188
189 pub(super) fn create_error_for_range(&self, range: Range, kind: ParseErrorKind) -> ParseError {
190 ParseError::new(range, kind, self.file_text)
191 }
192
193 fn parse_string(&mut self) -> Result<Token<'a>, ParseError> {
194 let quote = self.bytes[self.byte_index];
195 let start = self.byte_index + 1;
196
197 let mut i = start;
201 while i < self.bytes.len() {
202 let b = self.bytes[i];
203 if b == quote {
204 let s = &self.file_text[start..i];
206 self.byte_index = i + 1;
207 return Ok(Token::String(Cow::Borrowed(s)));
208 }
209 if b == b'\\' {
210 break;
211 }
212 i += 1;
213 }
214
215 let allow_extended_escapes = self.allow_extended_string_escapes;
217 crate::string::parse_string_with_char_provider(self, allow_extended_escapes)
218 .map(Token::String)
219 .map_err(|err| self.create_error_for_start(err.byte_index, ParseErrorKind::String(err.kind)))
221 }
222
223 fn parse_number(&mut self) -> Result<Token<'a>, ParseError> {
224 let start_byte_index = self.byte_index;
225
226 match self.bytes.get(self.byte_index) {
228 Some(b'+') => {
229 if !self.allow_unary_plus_numbers {
230 return Err(self.create_error_for_current_token(ParseErrorKind::UnaryPlusNumbersNotAllowed));
231 }
232 self.byte_index += 1;
233 }
234 Some(b'-') => {
235 self.byte_index += 1;
236 }
237 _ => {}
238 }
239
240 match self.bytes.get(self.byte_index) {
241 Some(b'0') => {
242 self.byte_index += 1;
243
244 if matches!(self.bytes.get(self.byte_index), Some(b'x' | b'X')) {
246 if !self.allow_hexadecimal_numbers {
247 return Err(self.create_error_for_current_token(ParseErrorKind::HexadecimalNumbersNotAllowed));
248 }
249
250 self.byte_index += 1;
251
252 if !matches!(self.bytes.get(self.byte_index), Some(b) if b.is_ascii_hexdigit()) {
254 return Err(self.create_error_for_current_char(ParseErrorKind::ExpectedDigit));
255 }
256
257 while matches!(self.bytes.get(self.byte_index), Some(b) if b.is_ascii_hexdigit()) {
258 self.byte_index += 1;
259 }
260
261 return Ok(Token::Number(&self.file_text[start_byte_index..self.byte_index]));
262 }
263 }
264 Some(b'1'..=b'9') => {
265 self.byte_index += 1;
266 while matches!(self.bytes.get(self.byte_index), Some(b'0'..=b'9')) {
267 self.byte_index += 1;
268 }
269 }
270 Some(b'.') if matches!(self.bytes.get(self.byte_index + 1), Some(b'0'..=b'9')) => {
271 if !self.allow_bare_decimal_point_numbers {
272 return Err(self.create_error_for_current_token(ParseErrorKind::BareDecimalPointNumbersNotAllowed));
273 }
274 }
275 Some(b'I' | b'N') => {
277 let word = if self.bytes[self.byte_index] == b'I' {
278 "Infinity"
279 } else {
280 "NaN"
281 };
282 if !self.try_move_word(word) {
283 return Err(self.create_error_for_current_char(ParseErrorKind::ExpectedDigitFollowingNegativeSign));
284 }
285 if !self.allow_non_finite_numbers {
286 return Err(self.create_error_for_current_token(ParseErrorKind::NonFiniteNumbersNotAllowed));
287 }
288 return Ok(Token::Number(&self.file_text[start_byte_index..self.byte_index]));
289 }
290 _ => {
291 return Err(self.create_error_for_current_char(ParseErrorKind::ExpectedDigitFollowingNegativeSign));
292 }
293 }
294
295 if self.bytes.get(self.byte_index) == Some(&b'.') {
296 self.byte_index += 1;
297
298 if !self.allow_bare_decimal_point_numbers && !matches!(self.bytes.get(self.byte_index), Some(b'0'..=b'9')) {
299 return Err(self.create_error_for_current_token(ParseErrorKind::BareDecimalPointNumbersNotAllowed));
300 }
301
302 while matches!(self.bytes.get(self.byte_index), Some(b'0'..=b'9')) {
303 self.byte_index += 1;
304 }
305 }
306
307 if matches!(self.bytes.get(self.byte_index), Some(b'e' | b'E')) {
308 self.byte_index += 1;
309
310 match self.bytes.get(self.byte_index) {
311 Some(b'-' | b'+') => {
312 self.byte_index += 1;
313 if !matches!(self.bytes.get(self.byte_index), Some(b'0'..=b'9')) {
314 return Err(self.create_error_for_current_char(ParseErrorKind::ExpectedDigit));
315 }
316 }
317 Some(b'0'..=b'9') => {}
318 _ => {
319 return Err(self.create_error_for_current_char(ParseErrorKind::ExpectedPlusMinusOrDigitInNumberLiteral));
320 }
321 }
322
323 while matches!(self.bytes.get(self.byte_index), Some(b'0'..=b'9')) {
324 self.byte_index += 1;
325 }
326 }
327
328 Ok(Token::Number(&self.file_text[start_byte_index..self.byte_index]))
329 }
330
331 fn parse_comment_line(&mut self) -> Token<'a> {
332 debug_assert!(self.bytes[self.byte_index] == b'/');
333 self.byte_index += 1;
334 debug_assert!(self.bytes[self.byte_index] == b'/');
335 let start_byte_index = self.byte_index + 1;
336 self.byte_index += 1;
337
338 while let Some(&b) = self.bytes.get(self.byte_index) {
341 if b == b'\n' || b == b'\r' {
342 break;
343 }
344 self.byte_index += 1;
345 }
346
347 Token::CommentLine(&self.file_text[start_byte_index..self.byte_index])
348 }
349
350 fn parse_comment_block(&mut self) -> Result<Token<'a>, ParseError> {
351 debug_assert!(self.bytes[self.byte_index] == b'/');
352 self.byte_index += 1;
353 debug_assert!(self.bytes[self.byte_index] == b'*');
354 let start_byte_index = self.byte_index + 1;
355 self.byte_index += 1;
356
357 loop {
359 match self.bytes.get(self.byte_index) {
360 Some(&b'*') if self.bytes.get(self.byte_index + 1) == Some(&b'/') => {
361 let end_byte_index = self.byte_index;
362 self.byte_index += 2;
363 return Ok(Token::CommentBlock(&self.file_text[start_byte_index..end_byte_index]));
364 }
365 Some(_) => self.byte_index += 1,
366 None => return Err(self.create_error_for_current_token(ParseErrorKind::UnterminatedCommentBlock)),
367 }
368 }
369 }
370
371 fn skip_whitespace(&mut self) {
372 while let Some(&b) = self.bytes.get(self.byte_index) {
373 if b <= b' ' {
374 match b {
375 b' ' | b'\t' | b'\n' | b'\r' | 0x0B | 0x0C => {
376 self.byte_index += 1;
377 continue;
378 }
379 _ => break,
380 }
381 } else if b >= 0x80 {
382 let c = self.file_text[self.byte_index..].chars().next().unwrap();
384 if c.is_whitespace() {
385 self.byte_index += c.len_utf8();
386 continue;
387 }
388 break;
389 } else {
390 break;
391 }
392 }
393 }
394
395 fn try_move_word(&mut self, text: &str) -> bool {
396 let text_bytes = text.as_bytes();
397 let end = self.byte_index + text_bytes.len();
398 if end > self.bytes.len() {
399 return false;
400 }
401 if &self.bytes[self.byte_index..end] != text_bytes {
402 return false;
403 }
404 if let Some(&next_byte) = self.bytes.get(end) {
406 if next_byte.is_ascii_alphanumeric() || next_byte == b'$' {
407 return false;
408 }
409 if next_byte >= 0x80
411 && let Some(c) = self.file_text[end..].chars().next()
412 && c.is_alphanumeric()
413 {
414 return false;
415 }
416 }
417 self.byte_index = end;
418 true
419 }
420
421 fn try_move_whole_word(&mut self, text: &str) -> bool {
424 if matches!(self.bytes.get(self.byte_index + text.len()), Some(b'-' | b'_')) {
425 return false;
426 }
427 self.try_move_word(text)
428 }
429
430 fn parse_word(&mut self) -> Result<Token<'a>, ParseError> {
431 let start_byte_index = self.byte_index;
432
433 while self.byte_index < self.bytes.len() {
434 let b = self.bytes[self.byte_index];
435 if b < 0x80 {
436 if b.is_ascii_whitespace() || b == b':' {
438 break;
439 }
440 if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'$' {
442 self.byte_index += 1;
443 } else {
444 return Err(self.create_error_for_current_token(ParseErrorKind::UnexpectedToken));
445 }
446 } else {
447 let c = self.file_text[self.byte_index..].chars().next().unwrap();
449 if c.is_whitespace() {
450 break;
451 }
452 if c.is_alphanumeric() {
453 self.byte_index += c.len_utf8();
454 } else {
455 return Err(self.create_error_for_current_token(ParseErrorKind::UnexpectedToken));
456 }
457 }
458 }
459
460 if self.byte_index == start_byte_index {
461 return Err(self.create_error_for_current_token(ParseErrorKind::UnexpectedToken));
462 }
463
464 Ok(Token::Word(&self.file_text[start_byte_index..self.byte_index]))
465 }
466
467 fn current_char(&self) -> Option<char> {
468 let &b = self.bytes.get(self.byte_index)?;
469 if b < 0x80 {
470 Some(b as char)
471 } else {
472 self.file_text[self.byte_index..].chars().next()
473 }
474 }
475
476 fn move_next_char(&mut self) -> Option<char> {
477 if self.byte_index >= self.bytes.len() {
478 return None;
479 }
480 let b = self.bytes[self.byte_index];
481 if b < 0x80 {
482 self.byte_index += 1;
483 } else {
484 let c = self.file_text[self.byte_index..].chars().next().unwrap();
485 self.byte_index += c.len_utf8();
486 }
487 self.current_char()
488 }
489}
490
491impl<'a> CharProvider<'a> for Scanner<'a> {
492 fn current_char(&mut self) -> Option<char> {
493 Scanner::current_char(self)
494 }
495
496 fn move_next_char(&mut self) -> Option<char> {
497 Scanner::move_next_char(self)
498 }
499
500 fn byte_index(&self) -> usize {
501 self.byte_index
502 }
503
504 fn text(&self) -> &'a str {
505 self.file_text
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use std::borrow::Cow;
512
513 use super::super::tokens::Token;
514 use super::*;
515 use pretty_assertions::assert_eq;
516
517 #[test]
518 fn it_tokenizes_string() {
519 assert_has_tokens(
520 r#""t\"est", "\t\r\n\n\u0020 test\n other","#,
521 vec![
522 Token::String(Cow::Borrowed(r#"t"est"#)),
523 Token::Comma,
524 Token::String(Cow::Borrowed("\t\r\n\n test\n other")),
525 Token::Comma,
526 ],
527 );
528 }
529
530 #[test]
531 fn it_errors_escaping_single_quote_in_double_quote() {
532 assert_has_error_with_options(
533 r#""t\'est""#,
534 "Invalid escape in double quote string on line 1 column 3",
535 &no_extended_string_escapes(),
536 );
537 }
538
539 #[test]
540 fn it_tokenizes_single_quote_string() {
541 assert_has_tokens(
542 r#"'t\'est','a',"#,
543 vec![
544 Token::String(Cow::Borrowed(r#"t'est"#)),
545 Token::Comma,
546 Token::String(Cow::Borrowed("a")),
547 Token::Comma,
548 ],
549 );
550 }
551
552 #[test]
553 fn it_errors_escaping_double_quote_in_single_quote() {
554 assert_has_error_with_options(
555 r#"'t\"est'"#,
556 "Invalid escape in single quote string on line 1 column 3",
557 &no_extended_string_escapes(),
558 );
559 }
560
561 #[test]
562 fn it_tokenizes_extended_string_escapes() {
563 assert_has_tokens(
564 "\"it\\'s\", 'say \\\"hi\\\"', '\\v\\0\\x41\\a', 'a\\\nb', 'a\\\rb', 'a\\\r\nb', 'a\\\u{2028}b', 'a\\\u{2029}b'",
565 vec![
566 Token::String(Cow::Borrowed("it's")),
567 Token::Comma,
568 Token::String(Cow::Borrowed("say \"hi\"")),
569 Token::Comma,
570 Token::String(Cow::Borrowed("\u{0B}\0Aa")),
571 Token::Comma,
572 Token::String(Cow::Borrowed("ab")),
573 Token::Comma,
574 Token::String(Cow::Borrowed("ab")),
575 Token::Comma,
576 Token::String(Cow::Borrowed("ab")),
577 Token::Comma,
578 Token::String(Cow::Borrowed("ab")),
579 Token::Comma,
580 Token::String(Cow::Borrowed("ab")),
581 ],
582 );
583 }
584
585 #[test]
586 fn it_errors_invalid_extended_string_escapes() {
587 assert_has_error(r#""\1""#, "Invalid escape on line 1 column 2");
588 assert_has_error(r#""\01""#, "Invalid escape on line 1 column 2");
589 assert_has_error(r#""\x4""#, "Expected two hex digits on line 1 column 2");
590 assert_has_error_with_options(
591 r#""\v""#,
592 "Invalid escape on line 1 column 2",
593 &no_extended_string_escapes(),
594 );
595 assert_has_error_with_options(
596 "\"a\\\nb\"",
597 "Invalid escape on line 1 column 3",
598 &no_extended_string_escapes(),
599 );
600 }
601
602 #[test]
603 fn it_ends_comment_line_at_a_lone_carriage_return() {
604 assert_has_tokens(
605 "//a\r1,//b\u{2028}2\n3",
606 vec![
607 Token::CommentLine("a"),
608 Token::Number("1"),
609 Token::Comma,
610 Token::CommentLine("b\u{2028}2"),
611 Token::Number("3"),
612 ],
613 );
614 assert_has_error("//x\r@", "Unexpected token on line 2 column 1");
615 assert_has_error("//x\r\n@", "Unexpected token on line 2 column 1");
616 }
617
618 fn no_extended_string_escapes() -> ScannerOptions {
619 ScannerOptions {
620 allow_extended_string_escapes: false,
621 ..Default::default()
622 }
623 }
624
625 #[test]
626 fn it_errors_for_word_starting_with_invalid_token() {
627 assert_has_error(r#"{ &test }"#, "Unexpected token on line 1 column 3");
628 }
629
630 #[test]
631 fn it_tokenizes_numbers() {
632 assert_has_tokens(
633 "0, 0.123, -198, 0e-345, 0.3e+025, 1e1,",
634 vec![
635 Token::Number("0"),
636 Token::Comma,
637 Token::Number("0.123"),
638 Token::Comma,
639 Token::Number("-198"),
640 Token::Comma,
641 Token::Number("0e-345"),
642 Token::Comma,
643 Token::Number("0.3e+025"),
644 Token::Comma,
645 Token::Number("1e1"),
646 Token::Comma,
647 ],
648 );
649 }
650
651 #[test]
652 fn it_tokenizes_hexadecimal_numbers() {
653 assert_has_tokens(
654 "0x7DF, 0xFF, 0x123ABC, 0xabc, 0X1F",
655 vec![
656 Token::Number("0x7DF"),
657 Token::Comma,
658 Token::Number("0xFF"),
659 Token::Comma,
660 Token::Number("0x123ABC"),
661 Token::Comma,
662 Token::Number("0xabc"),
663 Token::Comma,
664 Token::Number("0X1F"),
665 ],
666 );
667 }
668
669 #[test]
670 fn it_tokenizes_unary_plus_numbers() {
671 assert_has_tokens(
672 "+42, +0.5, +1e10, +0xFF",
673 vec![
674 Token::Number("+42"),
675 Token::Comma,
676 Token::Number("+0.5"),
677 Token::Comma,
678 Token::Number("+1e10"),
679 Token::Comma,
680 Token::Number("+0xFF"),
681 ],
682 );
683 }
684
685 #[test]
686 fn it_tokenizes_bare_decimal_point_numbers() {
687 assert_has_tokens(
688 ".5, -.5, +.5, 5., -5., 5.e3",
689 vec![
690 Token::Number(".5"),
691 Token::Comma,
692 Token::Number("-.5"),
693 Token::Comma,
694 Token::Number("+.5"),
695 Token::Comma,
696 Token::Number("5."),
697 Token::Comma,
698 Token::Number("-5."),
699 Token::Comma,
700 Token::Number("5.e3"),
701 ],
702 );
703 assert_has_error(".", "Unexpected token on line 1 column 1");
704 assert_has_error("-.", "Expected digit following negative sign on line 1 column 2");
705 }
706
707 #[test]
708 fn it_tokenizes_non_finite_numbers() {
709 assert_has_tokens(
710 "Infinity, -Infinity, +Infinity, NaN, -NaN, Infinityx",
711 vec![
712 Token::Number("Infinity"),
713 Token::Comma,
714 Token::Number("-Infinity"),
715 Token::Comma,
716 Token::Number("+Infinity"),
717 Token::Comma,
718 Token::Number("NaN"),
719 Token::Comma,
720 Token::Number("-NaN"),
721 Token::Comma,
722 Token::Word("Infinityx"),
723 ],
724 );
725 assert_has_error("-Infinit", "Expected digit following negative sign on line 1 column 2");
726 assert_has_tokens(
728 "-Infinity-1, -NaN_",
729 vec![
730 Token::Number("-Infinity"),
731 Token::Number("-1"),
732 Token::Comma,
733 Token::Number("-NaN"),
734 Token::Word("_"),
735 ],
736 );
737 }
738
739 #[test]
740 fn it_errors_invalid_exponent() {
741 assert_has_error(
742 r#"1ea"#,
743 "Expected plus, minus, or digit in number literal on line 1 column 3",
744 );
745 assert_has_error(r#"1e-a"#, "Expected digit on line 1 column 4");
746 }
747
748 #[test]
749 fn it_tokenizes_simple_tokens() {
750 assert_has_tokens(
751 "{}[],:true,false,null,",
752 vec![
753 Token::OpenBrace,
754 Token::CloseBrace,
755 Token::OpenBracket,
756 Token::CloseBracket,
757 Token::Comma,
758 Token::Colon,
759 Token::Boolean(true),
760 Token::Comma,
761 Token::Boolean(false),
762 Token::Comma,
763 Token::Null,
764 Token::Comma,
765 ],
766 );
767 }
768
769 #[test]
770 fn it_tokenizes_comment_line() {
771 assert_has_tokens(
772 "//test\n//t\r\n// test\n,",
773 vec![
774 Token::CommentLine("test"),
775 Token::CommentLine("t"),
776 Token::CommentLine(" test"),
777 Token::Comma,
778 ],
779 );
780 }
781
782 #[test]
783 fn it_tokenizes_comment_blocks() {
784 assert_has_tokens(
785 "/*test\n *//* test*/,",
786 vec![
787 Token::CommentBlock("test\n "),
788 Token::CommentBlock(" test"),
789 Token::Comma,
790 ],
791 );
792 }
793
794 #[test]
795 fn it_errors_on_invalid_utf8_char_for_issue_6() {
796 assert_has_error(
797 "\"\\uDF06\"",
798 "Invalid unicode escape sequence. 'DF06 (unpaired low surrogate)' is not a valid UTF8 character on line 1 column 2",
799 );
800 }
801
802 fn assert_has_tokens(text: &str, tokens: Vec<Token>) {
803 let mut scanner = Scanner::new(text, &Default::default());
804 let mut scanned_tokens = Vec::new();
805
806 loop {
807 match scanner.scan() {
808 Ok(Some(token)) => scanned_tokens.push(token),
809 Ok(None) => break,
810 Err(err) => panic!("Error parsing: {:?}", err),
811 }
812 }
813
814 assert_eq!(scanned_tokens, tokens);
815 }
816
817 fn assert_has_error(text: &str, message: &str) {
818 assert_has_error_with_options(text, message, &Default::default());
819 }
820
821 #[track_caller]
822 fn assert_has_error_with_options(text: &str, message: &str, options: &ScannerOptions) {
823 let mut scanner = Scanner::new(text, options);
824 let mut error_message = String::new();
825
826 loop {
827 match scanner.scan() {
828 Ok(Some(_)) => {}
829 Ok(None) => break,
830 Err(err) => {
831 error_message = err.to_string();
832 break;
833 }
834 }
835 }
836
837 assert_eq!(error_message, message);
838 }
839}