Skip to main content

jsonc_parser/
scanner.rs

1use std::borrow::Cow;
2
3use crate::string::CharProvider;
4
5use super::common::Range;
6use super::errors::*;
7use super::tokens::Token;
8
9/// Converts text into a stream of tokens.
10pub 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/// Options for the scanner.
25#[derive(Debug)]
26pub struct ScannerOptions {
27  /// Allow single-quoted strings (defaults to `true`).
28  pub allow_single_quoted_strings: bool,
29  /// Allow hexadecimal numbers like 0xFF (defaults to `true`).
30  pub allow_hexadecimal_numbers: bool,
31  /// Allow unary plus sign on numbers like +42 (defaults to `true`).
32  pub allow_unary_plus_numbers: bool,
33  /// Allow a leading or trailing decimal point on numbers like .5 or 5. (defaults to `true`).
34  pub allow_bare_decimal_point_numbers: bool,
35  /// Allow the numbers Infinity, -Infinity and NaN (defaults to `true`).
36  pub allow_non_finite_numbers: bool,
37  /// Allow JSON5 string escapes like \x41, \v and line continuations (defaults to `true`).
38  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  /// Creates a new scanner with specific options.
56  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  /// Moves to and returns the next token.
77  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  /// Gets the start position of the token.
142  pub fn token_start(&self) -> usize {
143    self.token_start
144  }
145
146  /// Gets the end position of the token.
147  pub fn token_end(&self) -> usize {
148    self.byte_index
149  }
150
151  /// Gets the current token.
152  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      // token was fully scanned — use the exact token end
159      self.byte_index
160    } else if let Some(c) = self.file_text[self.byte_index..].chars().next() {
161      // scanner hasn't advanced past token_start — cover the current character
162      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    // fast path: scan for closing quote or backslash byte-by-byte.
198    // this is safe because quote (0x22/0x27) and backslash (0x5C) are ASCII
199    // and can never appear as continuation bytes in multi-byte UTF-8 sequences.
200    let mut i = start;
201    while i < self.bytes.len() {
202      let b = self.bytes[i];
203      if b == quote {
204        // found closing quote with no escapes
205        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    // slow path: handle escape sequences via CharProvider
216    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      // todo(dsherret): don't convert the error kind to a string here
220      .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    // handle unary plus and unary minus
227    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        // check for hexadecimal literal (0x or 0X)
245        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          // must have at least one hex digit
253          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      // only reached after a sign, so unlike in `scan` there is no word to keep whole
276      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    // scan byte-by-byte for \n or \r, which are ASCII and can never appear as UTF-8 continuation bytes;
339    // U+2028 and U+2029 do not end the comment, matching other JSONC parsers
340    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    // scan byte-by-byte for */; both are ASCII and safe to scan through UTF-8
358    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        // handle non-ASCII unicode whitespace
383        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    // ensure the word is not followed by an alphanumeric character or `$`
405    if let Some(&next_byte) = self.bytes.get(end) {
406      if next_byte.is_ascii_alphanumeric() || next_byte == b'$' {
407        return false;
408      }
409      // check non-ASCII alphanumeric
410      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  /// Like `try_move_word`, but also refuses the characters `parse_word` continues a word with,
422  /// so a property name like `Infinity_count` or `NaN-key` still scans as one word.
423  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        // ASCII fast path
437        if b.is_ascii_whitespace() || b == b':' {
438          break;
439        }
440        // `$` is valid anywhere in a JSON5 (ECMAScript) identifier
441        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        // non-ASCII: decode char
448        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    // with missing commas, a signed keyword ends where `-1-1` would end
727    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}