Skip to main content

bamts_compiler/
scanner.rs

1//! The lexical scanner: total UTF-8 source text to a UTF-16-ranged token stream.
2//!
3//! The scanner is *total*: it accepts any `&str`, never panics, and guarantees
4//! forward progress, so every call to [`Scanner::next_token`] on a non-empty
5//! remainder consumes at least one code point. Trivia (whitespace, comments, a
6//! leading shebang) are emitted as tokens because [`crate::syntax::SourceFile`]
7//! preserves the full token stream.
8//!
9//! Token ranges are measured in UTF-16 code units so they line up with the
10//! coordinate space of [`SourceText`]. Positions are tracked incrementally as
11//! code points are consumed, so no per-token coordinate conversion is required.
12//!
13//! Two lexical forms cannot be decided by a raw left-to-right pass and are
14//! resolved through *explicit* scanner operations rather than guesses:
15//!
16//! * A `/` is ambiguous between division and a regular-expression literal. The
17//!   default pass always emits [`TokenKind::Slash`]/[`TokenKind::SlashEq`]; a
18//!   caller with grammar context calls [`Scanner::rescan_regex`] to reinterpret
19//!   it as a [`TokenKind::RegularExpressionLiteral`].
20//! * A `>` is ambiguous between a single relational operator and the start of a
21//!   shift/compound operator. The default pass greedily forms the longest
22//!   operator; a caller closing type arguments or a JSX tag calls
23//!   [`Scanner::rescan_greater_than`] to take exactly one `>`.
24//!
25//! Template literals are segmented in a single pass because the scanner tracks
26//! `{`/`}` nesting: a `}` that returns to a `${` boundary continues the template
27//! deterministically instead of being guessed. A parser that drives the scanner
28//! itself may instead call [`Scanner::rescan_template_continuation`].
29
30use std::sync::Arc;
31
32use crate::diagnostic::{Diagnostic, DiagnosticCode, Recovered};
33use crate::source::{ScriptKind, SourceId, SourceText, TextRange, Utf16Pos};
34use crate::syntax::{Token, TokenKind};
35
36/// Unterminated string literal.
37const UNTERMINATED_STRING: DiagnosticCode = DiagnosticCode::new("BAMTS-L001");
38/// Unterminated block comment.
39const UNTERMINATED_BLOCK_COMMENT: DiagnosticCode = DiagnosticCode::new("BAMTS-L002");
40/// Unterminated template literal.
41const UNTERMINATED_TEMPLATE: DiagnosticCode = DiagnosticCode::new("BAMTS-L003");
42/// Unterminated regular-expression literal.
43const UNTERMINATED_REGEX: DiagnosticCode = DiagnosticCode::new("BAMTS-L004");
44/// A character that cannot begin any token.
45const UNEXPECTED_CHARACTER: DiagnosticCode = DiagnosticCode::new("BAMTS-L005");
46/// A malformed escape sequence.
47const INVALID_ESCAPE: DiagnosticCode = DiagnosticCode::new("BAMTS-L006");
48/// A malformed unicode escape sequence.
49const INVALID_UNICODE_ESCAPE: DiagnosticCode = DiagnosticCode::new("BAMTS-L007");
50/// A misplaced numeric separator.
51const INVALID_NUMERIC_SEPARATOR: DiagnosticCode = DiagnosticCode::new("BAMTS-L008");
52/// A numeric literal with no valid digits.
53const INVALID_NUMERIC_LITERAL: DiagnosticCode = DiagnosticCode::new("BAMTS-L009");
54/// A `BigInt` suffix on a form that cannot be a `BigInt`.
55const INVALID_BIGINT_LITERAL: DiagnosticCode = DiagnosticCode::new("BAMTS-L010");
56/// A `#` that does not begin a private identifier.
57const INVALID_PRIVATE_IDENTIFIER: DiagnosticCode = DiagnosticCode::new("BAMTS-L011");
58
59/// The immutable product of one lexical pass over a source file.
60///
61/// It retains the file identity, the [`ScriptKind`], the shared source text, the
62/// non-EOF tokens in lexical order (including trivia), and the terminal
63/// end-of-file token whose empty range anchors the end of the source.
64#[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    /// Returns the source this token stream describes.
75    #[must_use]
76    pub const fn source_id(&self) -> SourceId {
77        self.source_id
78    }
79
80    /// Returns the syntax the source was scanned as.
81    #[must_use]
82    pub const fn script_kind(&self) -> ScriptKind {
83        self.script_kind
84    }
85
86    /// Returns the shared, immutable source text.
87    #[must_use]
88    pub fn source(&self) -> &Arc<SourceText> {
89        &self.source
90    }
91
92    /// Returns the source text mapper directly.
93    #[must_use]
94    pub fn source_text(&self) -> &SourceText {
95        &self.source
96    }
97
98    /// Returns the non-EOF tokens in lexical order, including trivia.
99    #[must_use]
100    pub fn tokens(&self) -> &[Token] {
101        &self.tokens
102    }
103
104    /// Returns the terminal end-of-file token.
105    #[must_use]
106    pub const fn eof(&self) -> &Token {
107        &self.eof
108    }
109
110    /// Returns the zero-copy lexeme for one token of this source.
111    ///
112    /// `None` identifies a range that is not a valid UTF-16 slice of this file,
113    /// which cannot arise from a scanner-produced token.
114    #[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/// Scans a whole source into an ordered token stream with recovery diagnostics.
127///
128/// This is the default single-pass driver: `/` stays division and `>` forms the
129/// longest operator, both of which a grammar-aware caller can reinterpret with
130/// [`Scanner::rescan_regex`] and [`Scanner::rescan_greater_than`]. Template
131/// literals are fully segmented here.
132#[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/// The kind of an open brace the scanner is currently inside, used to segment
162/// template literals in one pass without grammar feedback.
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164enum PendingBrace {
165    /// An ordinary `{` block or object literal.
166    Normal,
167    /// A `${` template substitution; its closing `}` continues the template.
168    Template,
169}
170
171/// A stateful lexical cursor over one immutable source text.
172///
173/// A caller may drive it token by token with [`Scanner::next_token`] and, at a
174/// grammatical decision point, request an explicit reinterpretation with one of
175/// the `rescan_*`/`scan_jsx_*` operations.
176pub 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    /// Creates a scanner positioned at the start of `source`.
190    #[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    /// Returns the syntax this scanner lexes.
206    #[must_use]
207    pub const fn script_kind(&self) -> ScriptKind {
208        self.script_kind
209    }
210
211    /// Returns the current UTF-16 cursor position.
212    #[must_use]
213    pub const fn position(&self) -> Utf16Pos {
214        Utf16Pos::new(self.utf16_pos)
215    }
216
217    /// Returns whether the whole source has been consumed.
218    #[must_use]
219    pub fn is_at_end(&self) -> bool {
220        self.byte_pos >= self.text.len()
221    }
222
223    /// Returns the diagnostics recorded so far, in emission order.
224    #[must_use]
225    pub fn diagnostics(&self) -> &[Diagnostic] {
226        &self.diagnostics
227    }
228
229    /// Consumes the scanner and returns its recorded diagnostics.
230    #[must_use]
231    pub fn into_diagnostics(self) -> Vec<Diagnostic> {
232        self.diagnostics
233    }
234
235    /// Scans the next token, or an end-of-file token at the end of the source.
236    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    /// Reinterprets the most recent `/`/`/=` token as a regular-expression
299    /// literal starting at the same position, advancing past its body and flags.
300    ///
301    /// This is the explicit division-versus-regex decision. The caller supplies
302    /// the grammatical context; the scanner never guesses it.
303    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    /// Reinterprets the most recent `>`-family token as a single `>`, advancing
311    /// exactly one code unit past the last token's start.
312    ///
313    /// A caller closing type arguments or a JSX element uses this to split a
314    /// greedily formed shift/compound operator.
315    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    /// Reinterprets the most recent `}` token as a template continuation,
323    /// producing [`TokenKind::TemplateMiddle`] or [`TokenKind::TemplateTail`].
324    ///
325    /// This serves a caller that drives the scanner without relying on the
326    /// single-pass brace tracking used by [`scan`].
327    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    /// Scans a run of JSX character data up to the next `<` or `{`.
335    ///
336    /// The token kind is [`TokenKind::StringLiteral`]: the fixed token space has
337    /// no dedicated JSX kind, and JSX text is uninterpreted character content
338    /// whose lexeme the caller reads directly. The run may be empty when the
339    /// caller is already positioned at a `<` or `{`.
340    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    /// Scans a JSX name, which unlike an ECMAScript identifier admits interior
354    /// hyphens (for example `data-role`).
355    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    /// Scans a JSX attribute string, which is delimited by matching quotes,
373    /// performs no escape processing, and may span line terminators.
374    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                // A raw CR or LF terminates a string; LS/PS are permitted.
475                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    /// Scans a template segment. With `continuation`, the cursor begins on the
494    /// `}` closing a substitution; otherwise it begins on the opening backtick.
495    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    /// Consumes a run of `radix` digits with ECMAScript numeric separators.
680    /// Returns whether at least one digit was consumed.
681    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        // Escaped identifiers are never keywords, matching the ECMAScript rule
742        // that a reserved word spelled with escapes is an ordinary identifier.
743        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            // A line continuation consumes the terminator; CRLF counts as one.
833            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    /// Reads a `\u`-style code point after the `u` has been consumed, handling
864    /// both the fixed four-digit and braced forms and reporting malformations.
865    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    /// Consumes exactly `count` hexadecimal digits, or as many as are present,
936    /// returning whether the full count was available.
937    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                // `?.` is optional chaining only when not followed by a digit,
1087                // so `x?.5` scans as `?` then `.5`.
1088                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
1171/// Returns whether a code point is scanner trivia whitespace.
1172///
1173/// This folds line terminators into whitespace; callers that need ASI decide by
1174/// inspecting the token lexeme. U+FEFF (byte-order mark / ZWNBSP) is treated as
1175/// whitespace, matching the ECMAScript `WhiteSpace` production.
1176fn 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
1184/// Returns whether a code point may begin an identifier.
1185///
1186/// This uses `char::is_alphabetic` plus `$` and `_` as a total, allocation-free
1187/// approximation of the Unicode `ID_Start` set, so classification never depends
1188/// on an external table crate.
1189fn is_id_start(c: char) -> bool {
1190    c == '$' || c == '_' || c.is_alphabetic()
1191}
1192
1193/// Returns whether a code point may continue an identifier.
1194///
1195/// This approximates `ID_Continue` with `char::is_alphanumeric` plus `$`, `_`,
1196/// and the ZWNJ/ZWJ joiners the ECMAScript grammar explicitly permits.
1197fn is_id_continue(c: char) -> bool {
1198    c == '$' || c == '_' || c == '\u{200C}' || c == '\u{200D}' || c.is_alphanumeric()
1199}
1200
1201/// Maps a raw, escape-free identifier lexeme to its reserved or contextual
1202/// keyword token, if any. The parser decides where contextual keywords are used
1203/// as ordinary identifiers.
1204fn 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    /// The stream must tile the whole source: adjacent, gap-free, and ending
1329    /// exactly at the source length, which the EOF token also anchors.
1330    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        // A `#` after the first byte is a private identifier, never a shebang.
1393        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        // `\u{69}f` spells `if` but escapes disqualify it from being a keyword.
1422        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        // `π` occupies two UTF-8 bytes but one UTF-16 unit.
1430        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        // `𝕏` is a single code point of length 2 in UTF-16.
1443        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); // quote + 2 units + quote
1448        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        // Recovery still tiles the source and resumes after the break.
1465        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        // A float or leading-zero integer cannot carry a BigInt suffix.
1511        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        // `x?.5` is `x`, `?`, `.5`, not `x`, `?.`, `5`.
1549        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        // `${ {a:1} }` nests an object literal whose braces must not close the
1572        // substitution early.
1573        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        // The character class keeps the interior `/` literal; flags follow.
1631        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        // The other quote is content, not a terminator.
1680        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        // A battery of hostile fragments must never panic and must always tile.
1698        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        // The default pass does not guess `/` as a regular expression, so a
1726        // file that uses a regex literal will not lex cleanly without the
1727        // explicit `rescan_regex` a parser would drive. These files still must
1728        // tile and round-trip; only the clean-diagnostics claim is waived.
1729        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            // Totality invariant: the stream tiles the source with no gaps and
1751            // the concatenated lexemes reproduce the source byte for byte.
1752            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                // The no-guess contract means the default pass reports at least
1777                // one diagnostic on a raw regex literal it cannot recognize.
1778                assert!(
1779                    !recovered.diagnostics().is_empty(),
1780                    "{name}: expected the default pass to reject a regex literal"
1781                );
1782            } else {
1783                // Every regex-free corpus driver is valid TypeScript and must
1784                // lex cleanly under the default pass.
1785                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}