1use std::sync::Arc;
31
32use crate::diagnostic::{Diagnostic, DiagnosticCode, Recovered};
33use crate::source::{ScriptKind, SourceId, SourceText, TextRange, Utf16Pos};
34use crate::syntax::{Token, TokenKind};
35
36const UNTERMINATED_STRING: DiagnosticCode = DiagnosticCode::new("BAMTS-L001");
38const UNTERMINATED_BLOCK_COMMENT: DiagnosticCode = DiagnosticCode::new("BAMTS-L002");
40const UNTERMINATED_TEMPLATE: DiagnosticCode = DiagnosticCode::new("BAMTS-L003");
42const UNTERMINATED_REGEX: DiagnosticCode = DiagnosticCode::new("BAMTS-L004");
44const UNEXPECTED_CHARACTER: DiagnosticCode = DiagnosticCode::new("BAMTS-L005");
46const INVALID_ESCAPE: DiagnosticCode = DiagnosticCode::new("BAMTS-L006");
48const INVALID_UNICODE_ESCAPE: DiagnosticCode = DiagnosticCode::new("BAMTS-L007");
50const INVALID_NUMERIC_SEPARATOR: DiagnosticCode = DiagnosticCode::new("BAMTS-L008");
52const INVALID_NUMERIC_LITERAL: DiagnosticCode = DiagnosticCode::new("BAMTS-L009");
54const INVALID_BIGINT_LITERAL: DiagnosticCode = DiagnosticCode::new("BAMTS-L010");
56const INVALID_PRIVATE_IDENTIFIER: DiagnosticCode = DiagnosticCode::new("BAMTS-L011");
58
59#[derive(Clone, Debug)]
65pub struct ScannedSource {
66 source_id: SourceId,
67 script_kind: ScriptKind,
68 source: Arc<SourceText>,
69 tokens: Vec<Token>,
70 eof: Token,
71}
72
73impl ScannedSource {
74 #[must_use]
76 pub const fn source_id(&self) -> SourceId {
77 self.source_id
78 }
79
80 #[must_use]
82 pub const fn script_kind(&self) -> ScriptKind {
83 self.script_kind
84 }
85
86 #[must_use]
88 pub fn source(&self) -> &Arc<SourceText> {
89 &self.source
90 }
91
92 #[must_use]
94 pub fn source_text(&self) -> &SourceText {
95 &self.source
96 }
97
98 #[must_use]
100 pub fn tokens(&self) -> &[Token] {
101 &self.tokens
102 }
103
104 #[must_use]
106 pub const fn eof(&self) -> &Token {
107 &self.eof
108 }
109
110 #[must_use]
115 pub fn token_text(&self, token: &Token) -> Option<&str> {
116 if token.is_missing() {
117 return Some("");
118 }
119 let range = token.range();
120 let start = self.source.utf16_to_byte(range.start()).ok()?;
121 let end = self.source.utf16_to_byte(range.end()).ok()?;
122 self.source.as_str().get(start..end)
123 }
124}
125
126#[must_use]
133pub fn scan(
134 source_id: SourceId,
135 script_kind: ScriptKind,
136 source: Arc<SourceText>,
137) -> Recovered<ScannedSource> {
138 let (tokens, eof, diagnostics) = {
139 let mut scanner = Scanner::new(source_id, script_kind, &source);
140 let mut tokens = Vec::new();
141 let eof = loop {
142 let token = scanner.next_token();
143 if token.kind() == TokenKind::EndOfFile {
144 break token;
145 }
146 tokens.push(token);
147 };
148 (tokens, eof, scanner.into_diagnostics())
149 };
150
151 let product = ScannedSource {
152 source_id,
153 script_kind,
154 source,
155 tokens,
156 eof,
157 };
158 Recovered::new(product, diagnostics)
159}
160
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164enum PendingBrace {
165 Normal,
167 Template,
169}
170
171pub struct Scanner<'a> {
177 source_id: SourceId,
178 script_kind: ScriptKind,
179 text: &'a str,
180 byte_pos: usize,
181 utf16_pos: usize,
182 last_start_byte: usize,
183 last_start_utf16: usize,
184 braces: Vec<PendingBrace>,
185 diagnostics: Vec<Diagnostic>,
186}
187
188impl<'a> Scanner<'a> {
189 #[must_use]
191 pub fn new(source_id: SourceId, script_kind: ScriptKind, source: &'a SourceText) -> Self {
192 Self {
193 source_id,
194 script_kind,
195 text: source.as_str(),
196 byte_pos: 0,
197 utf16_pos: 0,
198 last_start_byte: 0,
199 last_start_utf16: 0,
200 braces: Vec::new(),
201 diagnostics: Vec::new(),
202 }
203 }
204
205 #[must_use]
207 pub const fn script_kind(&self) -> ScriptKind {
208 self.script_kind
209 }
210
211 #[must_use]
213 pub const fn position(&self) -> Utf16Pos {
214 Utf16Pos::new(self.utf16_pos)
215 }
216
217 #[must_use]
219 pub fn is_at_end(&self) -> bool {
220 self.byte_pos >= self.text.len()
221 }
222
223 #[must_use]
225 pub fn diagnostics(&self) -> &[Diagnostic] {
226 &self.diagnostics
227 }
228
229 #[must_use]
231 pub fn into_diagnostics(self) -> Vec<Diagnostic> {
232 self.diagnostics
233 }
234
235 pub fn next_token(&mut self) -> Token {
237 let start_b = self.byte_pos;
238 let start_u = self.utf16_pos;
239 self.last_start_byte = start_b;
240 self.last_start_utf16 = start_u;
241
242 let Some(c) = self.first() else {
243 return self.make(TokenKind::EndOfFile, start_u);
244 };
245
246 let kind = match c {
247 _ if is_whitespace(c) => self.scan_whitespace(),
248 '/' => match self.second() {
249 Some('/') => self.scan_line_comment(),
250 Some('*') => self.scan_block_comment(start_u),
251 Some('=') => {
252 self.bump();
253 self.bump();
254 TokenKind::SlashEq
255 }
256 _ => {
257 self.bump();
258 TokenKind::Slash
259 }
260 },
261 '\'' | '"' => self.scan_string(c, start_u),
262 '`' => {
263 let kind = self.scan_template(start_u, false);
264 if kind == TokenKind::TemplateHead {
265 self.braces.push(PendingBrace::Template);
266 }
267 kind
268 }
269 '{' => {
270 self.bump();
271 self.braces.push(PendingBrace::Normal);
272 TokenKind::LBrace
273 }
274 '}' => match self.braces.pop() {
275 Some(PendingBrace::Template) => {
276 let kind = self.scan_template(start_u, true);
277 if kind == TokenKind::TemplateMiddle {
278 self.braces.push(PendingBrace::Template);
279 }
280 kind
281 }
282 _ => {
283 self.bump();
284 TokenKind::RBrace
285 }
286 },
287 '0'..='9' => self.scan_number(start_u),
288 '.' if self.second().is_some_and(|d| d.is_ascii_digit()) => self.scan_number(start_u),
289 '#' => self.scan_hash(start_b, start_u),
290 '\\' if self.second() == Some('u') => self.scan_identifier(start_b),
291 _ if is_id_start(c) => self.scan_identifier(start_b),
292 _ => self.scan_operator(c, start_u),
293 };
294
295 self.make(kind, start_u)
296 }
297
298 pub fn rescan_regex(&mut self) -> Token {
304 self.reset_to_last();
305 let start_u = self.utf16_pos;
306 let kind = self.scan_regex(start_u);
307 self.make(kind, start_u)
308 }
309
310 pub fn rescan_greater_than(&mut self) -> Token {
316 self.reset_to_last();
317 let start_u = self.utf16_pos;
318 self.bump();
319 self.make(TokenKind::GreaterThan, start_u)
320 }
321
322 pub fn rescan_template_continuation(&mut self) -> Token {
328 self.reset_to_last();
329 let start_u = self.utf16_pos;
330 let kind = self.scan_template(start_u, true);
331 self.make(kind, start_u)
332 }
333
334 pub fn scan_jsx_text(&mut self) -> Token {
341 let start_u = self.utf16_pos;
342 self.last_start_byte = self.byte_pos;
343 self.last_start_utf16 = start_u;
344 while let Some(c) = self.first() {
345 if c == '<' || c == '{' {
346 break;
347 }
348 self.bump();
349 }
350 self.make(TokenKind::StringLiteral, start_u)
351 }
352
353 pub fn scan_jsx_identifier(&mut self) -> Token {
356 let start_u = self.utf16_pos;
357 self.last_start_byte = self.byte_pos;
358 self.last_start_utf16 = start_u;
359 if self.first().is_some_and(is_id_start) {
360 self.bump();
361 while let Some(c) = self.first() {
362 if c == '-' || is_id_continue(c) {
363 self.bump();
364 } else {
365 break;
366 }
367 }
368 }
369 self.make(TokenKind::Identifier, start_u)
370 }
371
372 pub fn scan_jsx_attribute_string(&mut self) -> Token {
375 let start_u = self.utf16_pos;
376 self.last_start_byte = self.byte_pos;
377 self.last_start_utf16 = start_u;
378 let Some(quote @ ('\'' | '"')) = self.first() else {
379 self.error(
380 UNEXPECTED_CHARACTER,
381 start_u,
382 self.utf16_pos,
383 "a JSX attribute value must be a quoted string",
384 );
385 return self.make(TokenKind::StringLiteral, start_u);
386 };
387 self.bump();
388 loop {
389 match self.first() {
390 None => {
391 self.error(
392 UNTERMINATED_STRING,
393 start_u,
394 self.utf16_pos,
395 "unterminated string literal",
396 );
397 break;
398 }
399 Some(c) if c == quote => {
400 self.bump();
401 break;
402 }
403 Some(_) => {
404 self.bump();
405 }
406 }
407 }
408 self.make(TokenKind::StringLiteral, start_u)
409 }
410
411 fn scan_whitespace(&mut self) -> TokenKind {
412 while self.first().is_some_and(is_whitespace) {
413 self.bump();
414 }
415 TokenKind::Whitespace
416 }
417
418 fn scan_line_comment(&mut self) -> TokenKind {
419 self.bump();
420 self.bump();
421 while let Some(c) = self.first() {
422 if is_line_terminator(c) {
423 break;
424 }
425 self.bump();
426 }
427 TokenKind::LineComment
428 }
429
430 fn scan_block_comment(&mut self, start_u: usize) -> TokenKind {
431 self.bump();
432 self.bump();
433 loop {
434 match self.first() {
435 None => {
436 self.error(
437 UNTERMINATED_BLOCK_COMMENT,
438 start_u,
439 self.utf16_pos,
440 "unterminated block comment",
441 );
442 break;
443 }
444 Some('*') if self.second() == Some('/') => {
445 self.bump();
446 self.bump();
447 break;
448 }
449 Some(_) => {
450 self.bump();
451 }
452 }
453 }
454 TokenKind::BlockComment
455 }
456
457 fn scan_string(&mut self, quote: char, start_u: usize) -> TokenKind {
458 self.bump();
459 loop {
460 match self.first() {
461 None => {
462 self.error(
463 UNTERMINATED_STRING,
464 start_u,
465 self.utf16_pos,
466 "unterminated string literal",
467 );
468 break;
469 }
470 Some(c) if c == quote => {
471 self.bump();
472 break;
473 }
474 Some('\r' | '\n') => {
476 self.error(
477 UNTERMINATED_STRING,
478 start_u,
479 self.utf16_pos,
480 "unterminated string literal",
481 );
482 break;
483 }
484 Some('\\') => self.scan_escape(),
485 Some(_) => {
486 self.bump();
487 }
488 }
489 }
490 TokenKind::StringLiteral
491 }
492
493 fn scan_template(&mut self, start_u: usize, continuation: bool) -> TokenKind {
496 self.bump();
497 let closed = if continuation {
498 TokenKind::TemplateTail
499 } else {
500 TokenKind::NoSubstitutionTemplate
501 };
502 loop {
503 match self.first() {
504 None => {
505 self.error(
506 UNTERMINATED_TEMPLATE,
507 start_u,
508 self.utf16_pos,
509 "unterminated template literal",
510 );
511 return closed;
512 }
513 Some('`') => {
514 self.bump();
515 return closed;
516 }
517 Some('$') if self.second() == Some('{') => {
518 self.bump();
519 self.bump();
520 return if continuation {
521 TokenKind::TemplateMiddle
522 } else {
523 TokenKind::TemplateHead
524 };
525 }
526 Some('\\') => self.scan_escape(),
527 Some(_) => {
528 self.bump();
529 }
530 }
531 }
532 }
533
534 fn scan_regex(&mut self, start_u: usize) -> TokenKind {
535 self.bump();
536 let mut in_class = false;
537 loop {
538 match self.first() {
539 None => {
540 self.error(
541 UNTERMINATED_REGEX,
542 start_u,
543 self.utf16_pos,
544 "unterminated regular expression literal",
545 );
546 return TokenKind::RegularExpressionLiteral;
547 }
548 Some(c) if is_line_terminator(c) => {
549 self.error(
550 UNTERMINATED_REGEX,
551 start_u,
552 self.utf16_pos,
553 "unterminated regular expression literal",
554 );
555 return TokenKind::RegularExpressionLiteral;
556 }
557 Some('\\') => {
558 self.bump();
559 match self.first() {
560 None => {}
561 Some(c) if is_line_terminator(c) => {
562 self.error(
563 UNTERMINATED_REGEX,
564 start_u,
565 self.utf16_pos,
566 "unterminated regular expression literal",
567 );
568 return TokenKind::RegularExpressionLiteral;
569 }
570 Some(_) => {
571 self.bump();
572 }
573 }
574 }
575 Some('[') => {
576 in_class = true;
577 self.bump();
578 }
579 Some(']') => {
580 in_class = false;
581 self.bump();
582 }
583 Some('/') if !in_class => {
584 self.bump();
585 break;
586 }
587 Some(_) => {
588 self.bump();
589 }
590 }
591 }
592 while self.first().is_some_and(is_id_continue) {
593 self.bump();
594 }
595 TokenKind::RegularExpressionLiteral
596 }
597
598 fn scan_number(&mut self, start_u: usize) -> TokenKind {
599 let first = self.first().unwrap_or('0');
600 if first == '0' {
601 match self.second() {
602 Some('x' | 'X') => return self.scan_radix(16, start_u),
603 Some('o' | 'O') => return self.scan_radix(8, start_u),
604 Some('b' | 'B') => return self.scan_radix(2, start_u),
605 _ => {}
606 }
607 }
608
609 let legacy_octal_leading_zero =
610 first == '0' && self.second().is_some_and(|d| d.is_ascii_digit());
611 let mut is_integer = true;
612
613 if first == '.' {
614 is_integer = false;
615 self.bump();
616 self.consume_digits(10, start_u);
617 } else {
618 self.consume_digits(10, start_u);
619 if self.first() == Some('.') {
620 is_integer = false;
621 self.bump();
622 self.consume_digits(10, start_u);
623 }
624 }
625
626 if matches!(self.first(), Some('e' | 'E')) {
627 is_integer = false;
628 self.bump();
629 if matches!(self.first(), Some('+' | '-')) {
630 self.bump();
631 }
632 if !self.consume_digits(10, start_u) {
633 self.error(
634 INVALID_NUMERIC_LITERAL,
635 start_u,
636 self.utf16_pos,
637 "an exponent must have at least one digit",
638 );
639 }
640 }
641
642 if self.first() == Some('n') {
643 if is_integer && !legacy_octal_leading_zero {
644 self.bump();
645 return TokenKind::BigIntLiteral;
646 }
647 self.error(
648 INVALID_BIGINT_LITERAL,
649 start_u,
650 self.utf16_pos,
651 "a BigInt literal must be an integer without a leading zero",
652 );
653 self.bump();
654 return TokenKind::NumericLiteral;
655 }
656
657 TokenKind::NumericLiteral
658 }
659
660 fn scan_radix(&mut self, radix: u32, start_u: usize) -> TokenKind {
661 self.bump();
662 self.bump();
663 let any = self.consume_digits(radix, start_u);
664 if !any {
665 self.error(
666 INVALID_NUMERIC_LITERAL,
667 start_u,
668 self.utf16_pos,
669 "a numeric literal must have at least one digit",
670 );
671 }
672 if self.first() == Some('n') {
673 self.bump();
674 return TokenKind::BigIntLiteral;
675 }
676 TokenKind::NumericLiteral
677 }
678
679 fn consume_digits(&mut self, radix: u32, start_u: usize) -> bool {
682 let mut any = false;
683 let mut last_was_digit = false;
684 let mut trailing_separator = false;
685 loop {
686 match self.first() {
687 Some(c) if c.is_digit(radix) => {
688 self.bump();
689 any = true;
690 last_was_digit = true;
691 trailing_separator = false;
692 }
693 Some('_') => {
694 if !last_was_digit {
695 self.error(
696 INVALID_NUMERIC_SEPARATOR,
697 start_u,
698 self.utf16_pos,
699 "a numeric separator must sit between two digits",
700 );
701 }
702 self.bump();
703 last_was_digit = false;
704 trailing_separator = true;
705 }
706 _ => break,
707 }
708 }
709 if trailing_separator {
710 self.error(
711 INVALID_NUMERIC_SEPARATOR,
712 start_u,
713 self.utf16_pos,
714 "a numeric literal must not end with a separator",
715 );
716 }
717 any
718 }
719
720 fn scan_identifier(&mut self, start_b: usize) -> TokenKind {
721 let mut had_escape = false;
722 if self.first() == Some('\\') {
723 self.scan_identifier_escape(true);
724 had_escape = true;
725 } else {
726 self.bump();
727 }
728 loop {
729 match self.first() {
730 Some('\\') if self.second() == Some('u') => {
731 self.scan_identifier_escape(false);
732 had_escape = true;
733 }
734 Some(c) if is_id_continue(c) => {
735 self.bump();
736 }
737 _ => break,
738 }
739 }
740
741 if had_escape {
744 return TokenKind::Identifier;
745 }
746 let word = &self.text[start_b..self.byte_pos];
747 keyword_kind(word).unwrap_or(TokenKind::Identifier)
748 }
749
750 fn scan_identifier_escape(&mut self, is_start: bool) {
751 let esc_start = self.utf16_pos;
752 self.bump();
753 if self.first() != Some('u') {
754 self.error(
755 INVALID_UNICODE_ESCAPE,
756 esc_start,
757 self.utf16_pos,
758 "an identifier escape must be a unicode escape",
759 );
760 return;
761 }
762 self.bump();
763 if let Some(code_point) = self.read_hex_code_point(esc_start) {
764 let valid = char::try_from(code_point).ok().is_some_and(|character| {
765 if is_start {
766 is_id_start(character)
767 } else {
768 is_id_continue(character)
769 }
770 });
771 if !valid {
772 self.error(
773 INVALID_UNICODE_ESCAPE,
774 esc_start,
775 self.utf16_pos,
776 "the escaped code point is not a valid identifier character",
777 );
778 }
779 }
780 }
781
782 fn scan_hash(&mut self, start_b: usize, start_u: usize) -> TokenKind {
783 if start_b == 0 && self.second() == Some('!') {
784 self.bump();
785 self.bump();
786 while let Some(c) = self.first() {
787 if is_line_terminator(c) {
788 break;
789 }
790 self.bump();
791 }
792 return TokenKind::Shebang;
793 }
794
795 self.bump();
796 let begins_name = match self.first() {
797 Some('\\') => self.second() == Some('u'),
798 Some(c) => is_id_start(c),
799 None => false,
800 };
801 if begins_name {
802 if self.first() == Some('\\') {
803 self.scan_identifier_escape(true);
804 } else {
805 self.bump();
806 }
807 loop {
808 match self.first() {
809 Some('\\') if self.second() == Some('u') => self.scan_identifier_escape(false),
810 Some(c) if is_id_continue(c) => {
811 self.bump();
812 }
813 _ => break,
814 }
815 }
816 } else {
817 self.error(
818 INVALID_PRIVATE_IDENTIFIER,
819 start_u,
820 self.utf16_pos,
821 "a private identifier must have a name after `#`",
822 );
823 }
824 TokenKind::PrivateIdentifier
825 }
826
827 fn scan_escape(&mut self) {
828 let esc_start = self.utf16_pos;
829 self.bump();
830 match self.first() {
831 None => {}
832 Some('\r') => {
834 self.bump();
835 if self.first() == Some('\n') {
836 self.bump();
837 }
838 }
839 Some(c) if is_line_terminator(c) => {
840 self.bump();
841 }
842 Some('x') => {
843 self.bump();
844 if !self.consume_fixed_hex(2) {
845 self.error(
846 INVALID_ESCAPE,
847 esc_start,
848 self.utf16_pos,
849 "a hexadecimal escape requires two digits",
850 );
851 }
852 }
853 Some('u') => {
854 self.bump();
855 let _ = self.read_hex_code_point(esc_start);
856 }
857 Some(_) => {
858 self.bump();
859 }
860 }
861 }
862
863 fn read_hex_code_point(&mut self, esc_start: usize) -> Option<u32> {
866 if self.first() == Some('{') {
867 self.bump();
868 let mut value: u32 = 0;
869 let mut any = false;
870 let mut overflow = false;
871 while let Some(digit) = self.first().and_then(|c| c.to_digit(16)) {
872 self.bump();
873 any = true;
874 value = value.saturating_mul(16).saturating_add(digit);
875 if value > 0x0010_FFFF {
876 overflow = true;
877 }
878 }
879 if self.first() == Some('}') {
880 self.bump();
881 } else {
882 self.error(
883 INVALID_UNICODE_ESCAPE,
884 esc_start,
885 self.utf16_pos,
886 "a unicode escape is missing its closing brace",
887 );
888 return None;
889 }
890 if !any {
891 self.error(
892 INVALID_UNICODE_ESCAPE,
893 esc_start,
894 self.utf16_pos,
895 "a unicode escape has no digits",
896 );
897 return None;
898 }
899 if overflow {
900 self.error(
901 INVALID_UNICODE_ESCAPE,
902 esc_start,
903 self.utf16_pos,
904 "a unicode escape is greater than the maximum code point",
905 );
906 return None;
907 }
908 Some(value)
909 } else {
910 let mut value: u32 = 0;
911 let mut count = 0;
912 while count < 4 {
913 match self.first().and_then(|c| c.to_digit(16)) {
914 Some(digit) => {
915 self.bump();
916 value = value * 16 + digit;
917 count += 1;
918 }
919 None => break,
920 }
921 }
922 if count < 4 {
923 self.error(
924 INVALID_UNICODE_ESCAPE,
925 esc_start,
926 self.utf16_pos,
927 "a unicode escape requires four hexadecimal digits",
928 );
929 return None;
930 }
931 Some(value)
932 }
933 }
934
935 fn consume_fixed_hex(&mut self, count: usize) -> bool {
938 for _ in 0..count {
939 match self.first() {
940 Some(c) if c.is_ascii_hexdigit() => {
941 self.bump();
942 }
943 _ => return false,
944 }
945 }
946 true
947 }
948
949 fn scan_operator(&mut self, c: char, start_u: usize) -> TokenKind {
950 match c {
951 '(' => self.single(TokenKind::LParen),
952 ')' => self.single(TokenKind::RParen),
953 '[' => self.single(TokenKind::LBracket),
954 ']' => self.single(TokenKind::RBracket),
955 ',' => self.single(TokenKind::Comma),
956 ';' => self.single(TokenKind::Semicolon),
957 ':' => self.single(TokenKind::Colon),
958 '~' => self.single(TokenKind::Tilde),
959 '@' => self.single(TokenKind::At),
960 '.' => {
961 if self.second() == Some('.') && self.third() == Some('.') {
962 self.advance(3);
963 TokenKind::DotDotDot
964 } else {
965 self.single(TokenKind::Dot)
966 }
967 }
968 '+' => match self.second() {
969 Some('+') => self.pair(TokenKind::PlusPlus),
970 Some('=') => self.pair(TokenKind::PlusEq),
971 _ => self.single(TokenKind::Plus),
972 },
973 '-' => match self.second() {
974 Some('-') => self.pair(TokenKind::MinusMinus),
975 Some('=') => self.pair(TokenKind::MinusEq),
976 _ => self.single(TokenKind::Minus),
977 },
978 '*' => match self.second() {
979 Some('*') => {
980 if self.third() == Some('=') {
981 self.advance(3);
982 TokenKind::StarStarEq
983 } else {
984 self.pair(TokenKind::StarStar)
985 }
986 }
987 Some('=') => self.pair(TokenKind::StarEq),
988 _ => self.single(TokenKind::Star),
989 },
990 '%' => match self.second() {
991 Some('=') => self.pair(TokenKind::PercentEq),
992 _ => self.single(TokenKind::Percent),
993 },
994 '=' => match self.second() {
995 Some('=') => {
996 if self.third() == Some('=') {
997 self.advance(3);
998 TokenKind::EqEqEq
999 } else {
1000 self.pair(TokenKind::EqEq)
1001 }
1002 }
1003 Some('>') => self.pair(TokenKind::Arrow),
1004 _ => self.single(TokenKind::Eq),
1005 },
1006 '!' => match self.second() {
1007 Some('=') => {
1008 if self.third() == Some('=') {
1009 self.advance(3);
1010 TokenKind::BangEqEq
1011 } else {
1012 self.pair(TokenKind::BangEq)
1013 }
1014 }
1015 _ => self.single(TokenKind::Bang),
1016 },
1017 '<' => match self.second() {
1018 Some('<') => {
1019 if self.third() == Some('=') {
1020 self.advance(3);
1021 TokenKind::LessLessEq
1022 } else {
1023 self.pair(TokenKind::LessLess)
1024 }
1025 }
1026 Some('=') => self.pair(TokenKind::LessThanEq),
1027 _ => self.single(TokenKind::LessThan),
1028 },
1029 '>' => match self.second() {
1030 Some('>') => match self.third() {
1031 Some('>') => {
1032 if self.nth(3) == Some('=') {
1033 self.advance(4);
1034 TokenKind::GreaterGreaterGreaterEq
1035 } else {
1036 self.advance(3);
1037 TokenKind::GreaterGreaterGreater
1038 }
1039 }
1040 Some('=') => {
1041 self.advance(3);
1042 TokenKind::GreaterGreaterEq
1043 }
1044 _ => self.pair(TokenKind::GreaterGreater),
1045 },
1046 Some('=') => self.pair(TokenKind::GreaterThanEq),
1047 _ => self.single(TokenKind::GreaterThan),
1048 },
1049 '&' => match self.second() {
1050 Some('&') => {
1051 if self.third() == Some('=') {
1052 self.advance(3);
1053 TokenKind::AmpAmpEq
1054 } else {
1055 self.pair(TokenKind::AmpAmp)
1056 }
1057 }
1058 Some('=') => self.pair(TokenKind::AmpEq),
1059 _ => self.single(TokenKind::Amp),
1060 },
1061 '|' => match self.second() {
1062 Some('|') => {
1063 if self.third() == Some('=') {
1064 self.advance(3);
1065 TokenKind::PipePipeEq
1066 } else {
1067 self.pair(TokenKind::PipePipe)
1068 }
1069 }
1070 Some('=') => self.pair(TokenKind::PipeEq),
1071 _ => self.single(TokenKind::Pipe),
1072 },
1073 '^' => match self.second() {
1074 Some('=') => self.pair(TokenKind::CaretEq),
1075 _ => self.single(TokenKind::Caret),
1076 },
1077 '?' => match self.second() {
1078 Some('?') => {
1079 if self.third() == Some('=') {
1080 self.advance(3);
1081 TokenKind::QuestionQuestionEq
1082 } else {
1083 self.pair(TokenKind::QuestionQuestion)
1084 }
1085 }
1086 Some('.') if !self.third().is_some_and(|d| d.is_ascii_digit()) => {
1089 self.pair(TokenKind::QuestionDot)
1090 }
1091 _ => self.single(TokenKind::Question),
1092 },
1093 _ => {
1094 self.bump();
1095 self.error(
1096 UNEXPECTED_CHARACTER,
1097 start_u,
1098 self.utf16_pos,
1099 "this character cannot begin a token",
1100 );
1101 TokenKind::Unknown
1102 }
1103 }
1104 }
1105
1106 fn single(&mut self, kind: TokenKind) -> TokenKind {
1107 self.bump();
1108 kind
1109 }
1110
1111 fn pair(&mut self, kind: TokenKind) -> TokenKind {
1112 self.bump();
1113 self.bump();
1114 kind
1115 }
1116
1117 fn advance(&mut self, count: usize) {
1118 for _ in 0..count {
1119 if self.bump().is_none() {
1120 break;
1121 }
1122 }
1123 }
1124
1125 fn reset_to_last(&mut self) {
1126 self.byte_pos = self.last_start_byte;
1127 self.utf16_pos = self.last_start_utf16;
1128 }
1129
1130 fn rest(&self) -> &str {
1131 &self.text[self.byte_pos..]
1132 }
1133
1134 fn first(&self) -> Option<char> {
1135 self.rest().chars().next()
1136 }
1137
1138 fn second(&self) -> Option<char> {
1139 self.nth(1)
1140 }
1141
1142 fn third(&self) -> Option<char> {
1143 self.nth(2)
1144 }
1145
1146 fn nth(&self, index: usize) -> Option<char> {
1147 self.rest().chars().nth(index)
1148 }
1149
1150 fn bump(&mut self) -> Option<char> {
1151 let c = self.first()?;
1152 self.byte_pos += c.len_utf8();
1153 self.utf16_pos += c.len_utf16();
1154 Some(c)
1155 }
1156
1157 fn make(&self, kind: TokenKind, start_u: usize) -> Token {
1158 let range = TextRange::new(Utf16Pos::new(start_u), Utf16Pos::new(self.utf16_pos))
1159 .expect("scanner ranges advance monotonically");
1160 Token::new(kind, range)
1161 }
1162
1163 fn error(&mut self, code: DiagnosticCode, start_u: usize, end_u: usize, message: &'static str) {
1164 let range = TextRange::new(Utf16Pos::new(start_u), Utf16Pos::new(end_u))
1165 .expect("diagnostic ranges advance monotonically");
1166 self.diagnostics
1167 .push(Diagnostic::error(code, self.source_id, range, message));
1168 }
1169}
1170
1171fn is_whitespace(c: char) -> bool {
1177 c == '\u{FEFF}' || c.is_whitespace()
1178}
1179
1180fn is_line_terminator(c: char) -> bool {
1181 matches!(c, '\n' | '\r' | '\u{2028}' | '\u{2029}')
1182}
1183
1184fn is_id_start(c: char) -> bool {
1190 c == '$' || c == '_' || c.is_alphabetic()
1191}
1192
1193fn is_id_continue(c: char) -> bool {
1198 c == '$' || c == '_' || c == '\u{200C}' || c == '\u{200D}' || c.is_alphanumeric()
1199}
1200
1201fn keyword_kind(word: &str) -> Option<TokenKind> {
1205 Some(match word {
1206 "abstract" => TokenKind::KwAbstract,
1207 "accessor" => TokenKind::KwAccessor,
1208 "any" => TokenKind::KwAny,
1209 "as" => TokenKind::KwAs,
1210 "asserts" => TokenKind::KwAsserts,
1211 "async" => TokenKind::KwAsync,
1212 "await" => TokenKind::KwAwait,
1213 "bigint" => TokenKind::KwBigint,
1214 "boolean" => TokenKind::KwBoolean,
1215 "break" => TokenKind::KwBreak,
1216 "case" => TokenKind::KwCase,
1217 "catch" => TokenKind::KwCatch,
1218 "class" => TokenKind::KwClass,
1219 "const" => TokenKind::KwConst,
1220 "constructor" => TokenKind::KwConstructor,
1221 "continue" => TokenKind::KwContinue,
1222 "declare" => TokenKind::KwDeclare,
1223 "debugger" => TokenKind::KwDebugger,
1224 "default" => TokenKind::KwDefault,
1225 "delete" => TokenKind::KwDelete,
1226 "do" => TokenKind::KwDo,
1227 "else" => TokenKind::KwElse,
1228 "enum" => TokenKind::KwEnum,
1229 "export" => TokenKind::KwExport,
1230 "extends" => TokenKind::KwExtends,
1231 "false" => TokenKind::KwFalse,
1232 "finally" => TokenKind::KwFinally,
1233 "for" => TokenKind::KwFor,
1234 "from" => TokenKind::KwFrom,
1235 "function" => TokenKind::KwFunction,
1236 "get" => TokenKind::KwGet,
1237 "if" => TokenKind::KwIf,
1238 "implements" => TokenKind::KwImplements,
1239 "import" => TokenKind::KwImport,
1240 "in" => TokenKind::KwIn,
1241 "infer" => TokenKind::KwInfer,
1242 "instanceof" => TokenKind::KwInstanceof,
1243 "interface" => TokenKind::KwInterface,
1244 "is" => TokenKind::KwIs,
1245 "keyof" => TokenKind::KwKeyof,
1246 "let" => TokenKind::KwLet,
1247 "namespace" => TokenKind::KwNamespace,
1248 "never" => TokenKind::KwNever,
1249 "new" => TokenKind::KwNew,
1250 "null" => TokenKind::KwNull,
1251 "number" => TokenKind::KwNumber,
1252 "object" => TokenKind::KwObject,
1253 "of" => TokenKind::KwOf,
1254 "override" => TokenKind::KwOverride,
1255 "package" => TokenKind::KwPackage,
1256 "private" => TokenKind::KwPrivate,
1257 "protected" => TokenKind::KwProtected,
1258 "public" => TokenKind::KwPublic,
1259 "readonly" => TokenKind::KwReadonly,
1260 "return" => TokenKind::KwReturn,
1261 "satisfies" => TokenKind::KwSatisfies,
1262 "set" => TokenKind::KwSet,
1263 "static" => TokenKind::KwStatic,
1264 "string" => TokenKind::KwString,
1265 "super" => TokenKind::KwSuper,
1266 "switch" => TokenKind::KwSwitch,
1267 "symbol" => TokenKind::KwSymbol,
1268 "this" => TokenKind::KwThis,
1269 "throw" => TokenKind::KwThrow,
1270 "true" => TokenKind::KwTrue,
1271 "try" => TokenKind::KwTry,
1272 "type" => TokenKind::KwType,
1273 "typeof" => TokenKind::KwTypeof,
1274 "undefined" => TokenKind::KwUndefined,
1275 "unique" => TokenKind::KwUnique,
1276 "unknown" => TokenKind::KwUnknown,
1277 "var" => TokenKind::KwVar,
1278 "void" => TokenKind::KwVoid,
1279 "while" => TokenKind::KwWhile,
1280 "with" => TokenKind::KwWith,
1281 "yield" => TokenKind::KwYield,
1282 _ => return None,
1283 })
1284}
1285
1286#[cfg(test)]
1287mod tests {
1288 use super::*;
1289 use std::path::PathBuf;
1290
1291 fn scan_text(text: &str) -> Recovered<ScannedSource> {
1292 let source = Arc::new(SourceText::new(text));
1293 scan(SourceId::new(0), ScriptKind::TypeScript, source)
1294 }
1295
1296 fn kinds(text: &str) -> Vec<TokenKind> {
1297 scan_text(text)
1298 .into_product()
1299 .tokens()
1300 .iter()
1301 .map(Token::kind)
1302 .collect()
1303 }
1304
1305 fn significant(text: &str) -> Vec<(TokenKind, String)> {
1306 let product = scan_text(text).into_product();
1307 product
1308 .tokens()
1309 .iter()
1310 .filter(|token| {
1311 !matches!(
1312 token.kind(),
1313 TokenKind::Whitespace
1314 | TokenKind::LineComment
1315 | TokenKind::BlockComment
1316 | TokenKind::Shebang
1317 )
1318 })
1319 .map(|token| {
1320 (
1321 token.kind(),
1322 product.token_text(token).unwrap_or_default().to_string(),
1323 )
1324 })
1325 .collect()
1326 }
1327
1328 fn assert_tiles(text: &str) {
1331 let product = scan_text(text).into_product();
1332 let mut cursor = 0usize;
1333 for token in product.tokens() {
1334 assert_eq!(
1335 token.range().start().get(),
1336 cursor,
1337 "token {:?} left a gap in {text:?}",
1338 token.kind()
1339 );
1340 assert!(
1341 !token.range().is_empty(),
1342 "token {:?} made no forward progress in {text:?}",
1343 token.kind()
1344 );
1345 cursor = token.range().end().get();
1346 }
1347 let len = product.source_text().len_utf16().get();
1348 assert_eq!(cursor, len, "tokens did not reach end of {text:?}");
1349 assert_eq!(product.eof().range().start().get(), len);
1350 assert_eq!(product.eof().range().end().get(), len);
1351 assert_eq!(product.eof().kind(), TokenKind::EndOfFile);
1352 }
1353
1354 #[test]
1355 fn scanner_accepts_lone_surrogate_escape() {
1356 let recovered = scan_text("'\\uD800'");
1357 assert!(recovered.diagnostics().is_empty());
1358 assert_eq!(kinds("'\\uD800'"), vec![TokenKind::StringLiteral]);
1359 }
1360
1361 #[test]
1362 fn empty_source_has_only_eof() {
1363 let product = scan_text("").into_product();
1364 assert!(product.tokens().is_empty());
1365 assert_eq!(product.eof().kind(), TokenKind::EndOfFile);
1366 assert_eq!(product.eof().range().len(), 0);
1367 }
1368
1369 #[test]
1370 fn whitespace_and_newlines_fold_into_one_trivia_token() {
1371 assert_eq!(kinds(" \t\n\r\n "), vec![TokenKind::Whitespace]);
1372 assert_tiles(" \t\n\r\n ");
1373 }
1374
1375 #[test]
1376 fn line_and_block_comments_are_trivia() {
1377 assert_eq!(
1378 kinds("// hi\n/* a */"),
1379 vec![
1380 TokenKind::LineComment,
1381 TokenKind::Whitespace,
1382 TokenKind::BlockComment,
1383 ]
1384 );
1385 }
1386
1387 #[test]
1388 fn shebang_only_at_start() {
1389 assert_eq!(kinds("#!/usr/bin/env node\n"), {
1390 vec![TokenKind::Shebang, TokenKind::Whitespace]
1391 });
1392 assert_eq!(
1394 significant("a\n#!x")
1395 .iter()
1396 .map(|(kind, _)| *kind)
1397 .collect::<Vec<_>>(),
1398 vec![
1399 TokenKind::Identifier,
1400 TokenKind::PrivateIdentifier,
1401 TokenKind::Bang,
1402 TokenKind::Identifier,
1403 ]
1404 );
1405 }
1406
1407 #[test]
1408 fn keywords_are_distinct_from_identifiers() {
1409 assert_eq!(
1410 significant("const of asyncish"),
1411 vec![
1412 (TokenKind::KwConst, "const".into()),
1413 (TokenKind::KwOf, "of".into()),
1414 (TokenKind::Identifier, "asyncish".into()),
1415 ]
1416 );
1417 }
1418
1419 #[test]
1420 fn escaped_keyword_is_an_identifier() {
1421 let tokens = significant(r"\u{69}f");
1423 assert_eq!(tokens.len(), 1);
1424 assert_eq!(tokens[0].0, TokenKind::Identifier);
1425 }
1426
1427 #[test]
1428 fn unicode_identifier_ranges_are_utf16() {
1429 let product = scan_text("π=1").into_product();
1431 let ident = &product.tokens()[0];
1432 assert_eq!(ident.kind(), TokenKind::Identifier);
1433 assert_eq!(ident.range().start().get(), 0);
1434 assert_eq!(ident.range().end().get(), 1);
1435 assert_eq!(product.tokens()[1].kind(), TokenKind::Eq);
1436 assert_eq!(product.tokens()[1].range().start().get(), 1);
1437 assert_tiles("π=1");
1438 }
1439
1440 #[test]
1441 fn astral_characters_span_two_utf16_units() {
1442 let text = "\"𝕏\"";
1444 let product = scan_text(text).into_product();
1445 let string = &product.tokens()[0];
1446 assert_eq!(string.kind(), TokenKind::StringLiteral);
1447 assert_eq!(string.range().len(), 4); assert_eq!(product.token_text(string), Some(text));
1449 assert_tiles(text);
1450 }
1451
1452 #[test]
1453 fn strings_handle_escapes_and_report_unterminated() {
1454 assert_eq!(
1455 kinds(r#""a\"b\n\u{1F600}""#),
1456 vec![TokenKind::StringLiteral]
1457 );
1458 let recovered = scan_text("\"open\nnext");
1459 assert_eq!(
1460 recovered.diagnostics()[0].code(),
1461 UNTERMINATED_STRING,
1462 "a raw newline must terminate the string"
1463 );
1464 assert_tiles("\"open\nnext");
1466 }
1467
1468 #[test]
1469 fn unterminated_block_comment_is_diagnosed() {
1470 let recovered = scan_text("/* nope");
1471 assert_eq!(
1472 recovered.diagnostics()[0].code(),
1473 UNTERMINATED_BLOCK_COMMENT
1474 );
1475 assert_eq!(
1476 recovered.product().tokens()[0].kind(),
1477 TokenKind::BlockComment
1478 );
1479 }
1480
1481 #[test]
1482 fn numbers_cover_all_bases_and_bigint() {
1483 assert_eq!(kinds("0xFF"), vec![TokenKind::NumericLiteral]);
1484 assert_eq!(kinds("0o17"), vec![TokenKind::NumericLiteral]);
1485 assert_eq!(kinds("0b1010"), vec![TokenKind::NumericLiteral]);
1486 assert_eq!(kinds("1_000.5e-3"), vec![TokenKind::NumericLiteral]);
1487 assert_eq!(kinds(".25"), vec![TokenKind::NumericLiteral]);
1488 assert_eq!(kinds("123n"), vec![TokenKind::BigIntLiteral]);
1489 assert_eq!(kinds("0xFFn"), vec![TokenKind::BigIntLiteral]);
1490 }
1491
1492 #[test]
1493 fn malformed_numbers_are_diagnosed() {
1494 assert_eq!(
1495 scan_text("1__2").diagnostics()[0].code(),
1496 INVALID_NUMERIC_SEPARATOR
1497 );
1498 assert_eq!(
1499 scan_text("1_").diagnostics()[0].code(),
1500 INVALID_NUMERIC_SEPARATOR
1501 );
1502 assert_eq!(
1503 scan_text("1e").diagnostics()[0].code(),
1504 INVALID_NUMERIC_LITERAL
1505 );
1506 assert_eq!(
1507 scan_text("0x").diagnostics()[0].code(),
1508 INVALID_NUMERIC_LITERAL
1509 );
1510 assert_eq!(
1512 scan_text("1.5n").diagnostics()[0].code(),
1513 INVALID_BIGINT_LITERAL
1514 );
1515 }
1516
1517 #[test]
1518 fn operators_take_the_longest_match() {
1519 assert_eq!(
1520 kinds(">>>= >>> >>= >> >="),
1521 vec![
1522 TokenKind::GreaterGreaterGreaterEq,
1523 TokenKind::Whitespace,
1524 TokenKind::GreaterGreaterGreater,
1525 TokenKind::Whitespace,
1526 TokenKind::GreaterGreaterEq,
1527 TokenKind::Whitespace,
1528 TokenKind::GreaterGreater,
1529 TokenKind::Whitespace,
1530 TokenKind::GreaterThanEq,
1531 ]
1532 );
1533 assert_eq!(
1534 kinds("...a?.b??c"),
1535 vec![
1536 TokenKind::DotDotDot,
1537 TokenKind::Identifier,
1538 TokenKind::QuestionDot,
1539 TokenKind::Identifier,
1540 TokenKind::QuestionQuestion,
1541 TokenKind::Identifier,
1542 ]
1543 );
1544 }
1545
1546 #[test]
1547 fn optional_chain_before_digit_splits() {
1548 assert_eq!(
1550 kinds("x?.5"),
1551 vec![
1552 TokenKind::Identifier,
1553 TokenKind::Question,
1554 TokenKind::NumericLiteral,
1555 ]
1556 );
1557 }
1558
1559 #[test]
1560 fn private_identifier_and_missing_name() {
1561 assert_eq!(kinds("#field"), vec![TokenKind::PrivateIdentifier]);
1562 let recovered = scan_text("# ");
1563 assert_eq!(
1564 recovered.diagnostics()[0].code(),
1565 INVALID_PRIVATE_IDENTIFIER
1566 );
1567 }
1568
1569 #[test]
1570 fn templates_segment_with_nested_braces() {
1571 let text = "`h${ {a:1} }m${x}t`";
1574 assert_eq!(
1575 kinds(text),
1576 vec![
1577 TokenKind::TemplateHead,
1578 TokenKind::Whitespace,
1579 TokenKind::LBrace,
1580 TokenKind::Identifier,
1581 TokenKind::Colon,
1582 TokenKind::NumericLiteral,
1583 TokenKind::RBrace,
1584 TokenKind::Whitespace,
1585 TokenKind::TemplateMiddle,
1586 TokenKind::Identifier,
1587 TokenKind::TemplateTail,
1588 ]
1589 );
1590 assert_tiles(text);
1591 }
1592
1593 #[test]
1594 fn no_substitution_template() {
1595 assert_eq!(kinds("`plain`"), vec![TokenKind::NoSubstitutionTemplate]);
1596 }
1597
1598 #[test]
1599 fn unterminated_template_recovers() {
1600 let recovered = scan_text("`open");
1601 assert_eq!(recovered.diagnostics()[0].code(), UNTERMINATED_TEMPLATE);
1602 assert_eq!(
1603 recovered.product().tokens()[0].kind(),
1604 TokenKind::NoSubstitutionTemplate
1605 );
1606 }
1607
1608 #[test]
1609 fn default_pass_treats_slash_as_division() {
1610 assert_eq!(
1611 kinds("a / b"),
1612 vec![
1613 TokenKind::Identifier,
1614 TokenKind::Whitespace,
1615 TokenKind::Slash,
1616 TokenKind::Whitespace,
1617 TokenKind::Identifier,
1618 ]
1619 );
1620 }
1621
1622 #[test]
1623 fn rescan_regex_reinterprets_slash() {
1624 let source = Arc::new(SourceText::new(r"/ab[/]c/gi;"));
1625 let mut scanner = Scanner::new(SourceId::new(0), ScriptKind::JavaScript, &source);
1626 let slash = scanner.next_token();
1627 assert_eq!(slash.kind(), TokenKind::Slash);
1628 let regex = scanner.rescan_regex();
1629 assert_eq!(regex.kind(), TokenKind::RegularExpressionLiteral);
1630 assert_eq!(regex.range().start().get(), 0);
1632 assert_eq!(regex.range().end().get(), r"/ab[/]c/gi".len());
1633 let next = scanner.next_token();
1634 assert_eq!(next.kind(), TokenKind::Semicolon);
1635 }
1636
1637 #[test]
1638 fn rescan_regex_reports_unterminated() {
1639 let source = Arc::new(SourceText::new("/ab\nc"));
1640 let mut scanner = Scanner::new(SourceId::new(0), ScriptKind::JavaScript, &source);
1641 scanner.next_token();
1642 let regex = scanner.rescan_regex();
1643 assert_eq!(regex.kind(), TokenKind::RegularExpressionLiteral);
1644 assert_eq!(scanner.diagnostics()[0].code(), UNTERMINATED_REGEX);
1645 }
1646
1647 #[test]
1648 fn rescan_greater_than_splits_operator() {
1649 let source = Arc::new(SourceText::new(">>"));
1650 let mut scanner = Scanner::new(SourceId::new(0), ScriptKind::TypeScript, &source);
1651 let shift = scanner.next_token();
1652 assert_eq!(shift.kind(), TokenKind::GreaterGreater);
1653 let single = scanner.rescan_greater_than();
1654 assert_eq!(single.kind(), TokenKind::GreaterThan);
1655 assert_eq!(single.range().len(), 1);
1656 let rest = scanner.next_token();
1657 assert_eq!(rest.kind(), TokenKind::GreaterThan);
1658 }
1659
1660 #[test]
1661 fn jsx_operations_scan_text_names_and_attribute_strings() {
1662 let source = Arc::new(SourceText::new("hello world<"));
1663 let mut scanner = Scanner::new(SourceId::new(0), ScriptKind::TypeScriptReact, &source);
1664 let text = scanner.scan_jsx_text();
1665 assert_eq!(text.kind(), TokenKind::StringLiteral);
1666 assert_eq!(text.range().end().get(), "hello world".len());
1667 assert_eq!(scanner.next_token().kind(), TokenKind::LessThan);
1668
1669 let names = Arc::new(SourceText::new("data-role="));
1670 let mut scanner = Scanner::new(SourceId::new(0), ScriptKind::TypeScriptReact, &names);
1671 let name = scanner.scan_jsx_identifier();
1672 assert_eq!(name.kind(), TokenKind::Identifier);
1673 assert_eq!(name.range().end().get(), "data-role".len());
1674
1675 let attr = Arc::new(SourceText::new("'a\"b'"));
1676 let mut scanner = Scanner::new(SourceId::new(0), ScriptKind::TypeScriptReact, &attr);
1677 let value = scanner.scan_jsx_attribute_string();
1678 assert_eq!(value.kind(), TokenKind::StringLiteral);
1679 assert_eq!(value.range().len(), 5);
1681 }
1682
1683 #[test]
1684 fn unexpected_character_makes_progress() {
1685 let recovered = scan_text("\u{7}a");
1686 assert_eq!(recovered.diagnostics()[0].code(), UNEXPECTED_CHARACTER);
1687 assert_eq!(recovered.product().tokens()[0].kind(), TokenKind::Unknown);
1688 assert_eq!(
1689 recovered.product().tokens()[1].kind(),
1690 TokenKind::Identifier
1691 );
1692 assert_tiles("\u{7}a");
1693 }
1694
1695 #[test]
1696 fn scanner_is_total_over_arbitrary_inputs() {
1697 let fragments = [
1699 "",
1700 "\\",
1701 "\\u",
1702 "\\u{",
1703 "\\u{ZZ}",
1704 "0x",
1705 "'\\",
1706 "`${",
1707 "}",
1708 "/*",
1709 "/",
1710 "#",
1711 "\u{2028}\u{2029}",
1712 "𝕏\\u{1F4A9}n",
1713 "\"\\x1\"",
1714 "1_2_3n",
1715 "aaaa",
1716 "?.?.??=>>>=",
1717 ];
1718 for fragment in fragments {
1719 assert_tiles(fragment);
1720 }
1721 }
1722
1723 #[test]
1724 fn corpus_cases_lex_totally_and_tile() {
1725 const REGEX_LITERAL_CASES: &[&str] = &["escape-string-regexp.ts"];
1730
1731 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cases");
1732 let mut scanned_any = false;
1733 let mut regex_case_seen = false;
1734 for entry in std::fs::read_dir(&root).expect("corpus/cases must be readable") {
1735 let path = entry.expect("directory entry").path();
1736 if path.extension().and_then(|ext| ext.to_str()) != Some("ts") {
1737 continue;
1738 }
1739 scanned_any = true;
1740 let name = path
1741 .file_name()
1742 .and_then(|name| name.to_str())
1743 .unwrap_or_default()
1744 .to_string();
1745 let text = std::fs::read_to_string(&path).expect("corpus case is UTF-8");
1746 let source = Arc::new(SourceText::new(text.clone()));
1747 let recovered = scan(SourceId::new(0), ScriptKind::TypeScript, source);
1748 let product = recovered.product();
1749
1750 let mut cursor = 0usize;
1753 let mut rebuilt = String::with_capacity(text.len());
1754 for token in product.tokens() {
1755 assert_eq!(
1756 token.range().start().get(),
1757 cursor,
1758 "{name}: gap before {:?}",
1759 token.kind()
1760 );
1761 cursor = token.range().end().get();
1762 rebuilt.push_str(product.token_text(token).expect("token maps to a lexeme"));
1763 }
1764 assert_eq!(
1765 cursor,
1766 product.source_text().len_utf16().get(),
1767 "{name}: stream did not reach end of source"
1768 );
1769 assert_eq!(
1770 rebuilt, text,
1771 "{name}: lexemes did not reproduce the source"
1772 );
1773
1774 if REGEX_LITERAL_CASES.contains(&name.as_str()) {
1775 regex_case_seen = true;
1776 assert!(
1779 !recovered.diagnostics().is_empty(),
1780 "{name}: expected the default pass to reject a regex literal"
1781 );
1782 } else {
1783 assert!(
1786 recovered.diagnostics().is_empty(),
1787 "{name}: unexpected diagnostics {:?}",
1788 recovered.diagnostics()
1789 );
1790 }
1791 }
1792 assert!(scanned_any, "expected at least one corpus case");
1793 assert!(
1794 regex_case_seen,
1795 "expected the regex-literal case to be present"
1796 );
1797 }
1798}