Skip to main content

hermes_parser/lexer/
mod.rs

1//! JSLexer, a faithful port of `lib/Parser/JSLexer.cpp`.
2//!
3//! `JSLexer` lexes the full JavaScript token surface: punctuators, trivia
4//! (whitespace and line/block comments), identifiers and keywords, numeric
5//! literals, string literals, template literals, regular-expression literals,
6//! private identifiers, JSX, and the Flow type context. It also exposes the
7//! stateful lexer APIs: optional comment/token storage, magic comment
8//! (`sourceURL`/`sourceMappingURL`) extraction, `SavePoint` for backtracking,
9//! the directive (`"use strict"`) check, and `rescanRBrace` for template
10//! continuations. The `impl<'a> JSLexer<'a>` methods are split across the child
11//! modules below by concern (escape, identifier, number, string, template,
12//! regexp, jsx, dump, state).
13
14// Each child module can see the private fields of `JSLexer` (privacy in Rust is
15// "visible to the declaring module and its descendants"), so no field needs to
16// be made more public to support the split. Methods called across module
17// boundaries are `pub(crate)`.
18mod dump;
19mod escape;
20mod identifier;
21mod jsx;
22mod lookahead;
23mod number;
24mod regexp;
25mod state;
26mod string;
27mod template;
28
29pub use state::SavePoint;
30
31use std::rc::Rc;
32
33use hermes_atom_table::{AtomBytes, AtomTable};
34use hermes_support::buffer::SourceBuffer;
35use hermes_support::diag::Subsystem;
36use hermes_support::location::{SMLoc, SMRange, SourceId};
37use hermes_support::manager::SourceErrorManager;
38
39use hermes_unicode::{
40    is_unicode_id_start, is_unicode_only_id_start, is_unicode_only_space,
41};
42
43use crate::cursor::Cursor;
44use crate::token::{CommentKind, StoredComment, StoredToken, Token};
45use crate::token_kinds::TokenKind;
46use crate::utf8::{
47    append_unicode_to_storage, convert_utf16_to_utf8_with_replacements,
48    convert_utf8_with_surrogates_to_utf16, decode_utf8,
49    match_unicode_line_terminator_offset1, UTF8_LINE_TERMINATOR_CHAR0,
50};
51
52/// The grammar context affecting how some tokens are lexed (e.g. `/` as a
53/// division operator vs. a regular-expression literal). Port of
54/// `JSLexer::GrammarContext`.
55#[derive(Copy, Clone, Eq, PartialEq, Debug)]
56pub enum GrammarContext {
57    /// A RegExp can follow, so `/` starts a regular-expression literal.
58    AllowRegExp,
59    /// `/` can follow, so it is scanned as the division operator.
60    AllowDiv,
61    /// `/` can follow, `-` is part of identifiers, and `>` is scanned as its
62    /// own token.
63    AllowJSXIdentifier,
64    /// A type annotation: `/` can follow, `>>` scans as two separate `>`
65    /// tokens, and legacy octal literals are rejected as in strict mode.
66    Type,
67}
68
69/// The identifier-scanning mode, port of `JSLexer::IdentifierMode`. Affects
70/// which extra characters are accepted as identifier parts: JSX accepts `-`,
71/// Flow accepts `@`.
72#[derive(Copy, Clone, Eq, PartialEq, Debug)]
73pub enum IdentifierMode {
74    /// Standard JavaScript identifiers only.
75    JS,
76    /// JavaScript identifiers and '-'.
77    JSX,
78    /// JavaScript identifiers and identifiers which begin with '@'.
79    Flow,
80}
81
82/// Type-level marker for the identifier-scanning mode: the Rust analog of the
83/// C++ `template <IdentifierMode Mode>` non-type template parameter. Each impl
84/// pins `MODE` to a compile-time constant, so the per-character mode checks in
85/// the identifier scan loops (`M::MODE == IdentifierMode::JSX`, etc.) fold away
86/// in every monomorphization — matching the C++ template specializations rather
87/// than testing a runtime `mode` argument inside the inner loop.
88pub(crate) trait IdMode {
89    const MODE: IdentifierMode;
90}
91/// Standard JavaScript identifiers only.
92pub(crate) struct JsMode;
93impl IdMode for JsMode {
94    const MODE: IdentifierMode = IdentifierMode::JS;
95}
96/// JavaScript identifiers and '-'.
97pub(crate) struct JsxMode;
98impl IdMode for JsxMode {
99    const MODE: IdentifierMode = IdentifierMode::JSX;
100}
101/// JavaScript identifiers and identifiers which begin with '@'.
102pub(crate) struct FlowMode;
103impl IdMode for FlowMode {
104    const MODE: IdentifierMode = IdentifierMode::Flow;
105}
106
107/// The Hermes JavaScript lexer. Port of `hermes::parser::JSLexer`.
108///
109/// The lexer borrows the `SourceErrorManager` (for diagnostics) and the
110/// `AtomTable` interner, and owns a `Cursor` over a clone of the source buffer.
111pub struct JSLexer<'a> {
112    sm: &'a mut SourceErrorManager,
113    /// ID of the buffer in the SourceErrorManager.
114    buf_id: SourceId,
115    /// The scan cursor over the (NUL-terminated) source buffer.
116    cursor: Cursor,
117    /// Interner shared with the rest of the front end.
118    strtab: &'a AtomTable,
119
120    /// Pre-interned reserved-word identifiers, indexed by
121    /// `ord(kind) - ord(_first_resword)`. Port of `resWordIdent_`.
122    res_word_idents: Vec<AtomBytes>,
123
124    /// The current token.
125    token: Token,
126    /// The end location of the previous token (port of `prevTokenEndLoc_`).
127    prev_token_end: SMLoc,
128    /// True if there was a line terminator before the current token.
129    new_line_before_current_token: bool,
130
131    /// Whether the lexer is in strict mode (affects reserved-word recognition
132    /// in the identifier scanner and octal handling in the number/escape
133    /// scanners). Port of `strictMode_`.
134    strict_mode: bool,
135    /// Whether to convert surrogate pairs while decoding. Port of
136    /// `convertSurrogates_`.
137    convert_surrogates: bool,
138
139    /// Scratch storage for assembling identifier/string/regexp values. Port of
140    /// `tmpStorage_`.
141    tmp_storage: Vec<u8>,
142
143    /// Scratch storage for assembling the Template Raw Value (TRV) of template
144    /// literals. Port of `rawStorage_`.
145    raw_storage: Vec<u8>,
146
147    /// `//# sourceURL=` value, if seen (port of `sourceURL_`).
148    source_url: Option<String>,
149    /// `//# sourceMappingURL=` value, if seen (port of `sourceMappingURL_`).
150    source_mapping_url: Option<String>,
151
152    /// Whether to store comments encountered while lexing instead of skipping
153    /// them. Port of `storeComments_`.
154    store_comments: bool,
155    /// Stored comments (only populated when `store_comments`). Port of
156    /// `commentStorage_`.
157    comment_storage: Vec<StoredComment>,
158    /// Whether to store every token encountered while lexing. Port of
159    /// `storeTokens_`.
160    store_tokens: bool,
161    /// Stored tokens (only populated when `store_tokens`). Port of
162    /// `tokenStorage_`.
163    token_storage: Vec<StoredToken>,
164}
165
166impl<'a> JSLexer<'a> {
167    /// Construct a lexer over the buffer identified by `buf_id` in `sm`.
168    /// Port of `JSLexer::JSLexer` + `initializeWithBufferId`. The reserved-word
169    /// pre-interning (`initializeReservedIdentifiers`) is performed here so that
170    /// `res_word_ident` is a cheap lookup during identifier scanning.
171    pub fn new(
172        buf_id: SourceId,
173        sm: &'a mut SourceErrorManager,
174        strtab: &'a AtomTable,
175        grammar_context: GrammarContext,
176    ) -> JSLexer<'a> {
177        JSLexer::new_with_convert_surrogates(
178            buf_id,
179            sm,
180            strtab,
181            grammar_context,
182            false,
183        )
184    }
185
186    /// Like `new`, but with control over the `convert_surrogates` option. When
187    /// `convert_surrogates` is set, `get_string_literal` re-encodes the internal
188    /// WTF-8 string form into valid UTF-8 (combining surrogate pairs and
189    /// replacing unpaired surrogates with U+FFFD). Port of the `JSLexer`
190    /// constructor's `convertSurrogates` parameter.
191    pub fn new_with_convert_surrogates(
192        buf_id: SourceId,
193        sm: &'a mut SourceErrorManager,
194        strtab: &'a AtomTable,
195        _grammar_context: GrammarContext,
196        convert_surrogates: bool,
197    ) -> JSLexer<'a> {
198        let buffer: Rc<SourceBuffer> = sm.source_buffer(buf_id);
199        let cursor = Cursor::new(buffer);
200        let start = SMLoc {
201            source: buf_id,
202            offset: 0,
203        };
204        let mut lexer = JSLexer {
205            sm,
206            buf_id,
207            cursor,
208            strtab,
209            res_word_idents: Vec::new(),
210            token: Token::new(buf_id),
211            prev_token_end: start,
212            new_line_before_current_token: false,
213            strict_mode: true,
214            convert_surrogates,
215            tmp_storage: Vec::new(),
216            raw_storage: Vec::new(),
217            source_url: None,
218            source_mapping_url: None,
219            store_comments: false,
220            comment_storage: Vec::new(),
221            store_tokens: false,
222            token_storage: Vec::new(),
223        };
224        lexer.initialize_reserved_identifiers();
225        lexer
226    }
227
228    /// Re-encode the internal WTF-8 string `bytes` (which may contain lone
229    /// surrogates / surrogate-encoded astral characters) into *valid* UTF-8,
230    /// combining surrogate pairs into supplementary-plane characters and
231    /// replacing unpaired surrogates with U+FFFD, then intern the result. Port
232    /// of `convertSurrogatesInString` (JSLexer.cpp:2486-2495).
233    fn convert_surrogates_in_string(&self, bytes: &[u8]) -> AtomBytes {
234        let ustr = convert_utf8_with_surrogates_to_utf16(bytes);
235        let output = convert_utf16_to_utf8_with_replacements(&ustr);
236        self.strtab.atom_bytes(output)
237    }
238
239    /// Intern a string-literal value, applying the `convert_surrogates`
240    /// re-encoding when the option is set. Port of `getStringLiteral`
241    /// (JSLexer.h:689-694).
242    pub fn get_string_literal(&self, bytes: &[u8]) -> AtomBytes {
243        if self.convert_surrogates {
244            self.convert_surrogates_in_string(bytes)
245        } else {
246            self.strtab.atom_bytes(bytes)
247        }
248    }
249
250    /// Pre-intern all reserved words so that `res_word_ident` is a cheap lookup.
251    /// Port of `initializeReservedIdentifiers` (JSLexer.cpp:111-115).
252    fn initialize_reserved_identifiers(&mut self) {
253        use crate::token_kinds::{ord, token_kind_str_by_ord, TokenKind};
254        let first = ord(TokenKind::_first_resword);
255        let last = ord(TokenKind::_last_resword);
256        // Index by `ord(kind) - ord(_first_resword)`. We allocate one slot per
257        // ordinal in the inclusive marker range so `res_word_ident` can index
258        // directly; the two marker slots are never read.
259        let count = (last - first + 1) as usize;
260        self.res_word_idents = Vec::with_capacity(count);
261        for v in first..=last {
262            // The marker slots (`_first_resword`/`_last_resword`) get an empty
263            // placeholder; every real reserved word interns its name.
264            let name = if v > first && v < last {
265                token_kind_str_by_ord(v)
266            } else {
267                ""
268            };
269            self.res_word_idents
270                .push(self.strtab.atom_bytes(name.as_bytes()));
271        }
272    }
273
274    /// \return the pre-interned identifier for reserved word `kind`. Port of
275    /// `resWordIdent` (JSLexer.h:445-449).
276    pub(crate) fn res_word_ident(&self, kind: TokenKind) -> AtomBytes {
277        use crate::token_kinds::{ord, TokenKind as TK};
278        debug_assert!(kind.is_res_word());
279        self.res_word_idents[(ord(kind) - ord(TK::_first_resword)) as usize]
280    }
281
282    /// \return the current token.
283    pub fn token(&self) -> &Token {
284        &self.token
285    }
286
287    /// \return whether the lexer is in strict mode. Port of `isStrictMode`.
288    pub fn is_strict_mode(&self) -> bool {
289        self.strict_mode
290    }
291
292    /// Set strict mode (affects future-reserved-word recognition). Port of
293    /// `setStrictMode`.
294    pub fn set_strict_mode(&mut self, strict_mode: bool) {
295        self.strict_mode = strict_mode;
296    }
297
298    /// \return whether a line terminator preceded the current token.
299    pub fn is_new_line_before_current_token(&self) -> bool {
300        self.new_line_before_current_token
301    }
302
303    /// Set whether comments should be stored instead of skipped. Port of
304    /// `setStoreComments`.
305    pub fn set_store_comments(&mut self, store_comments: bool) {
306        self.store_comments = store_comments;
307    }
308
309    /// \return whether tokens are being stored. Port of `getStoreTokens`.
310    pub fn get_store_tokens(&self) -> bool {
311        self.store_tokens
312    }
313
314    /// Set whether every token should be stored as it is lexed. Port of
315    /// `setStoreTokens`.
316    pub fn set_store_tokens(&mut self, store_tokens: bool) {
317        self.store_tokens = store_tokens;
318    }
319
320    /// Unconditionally store the current token in the token storage. Port of
321    /// `storeCurrentToken` (JSLexer.h:548-551).
322    pub fn store_current_token(&mut self) {
323        debug_assert!(
324            self.store_tokens,
325            "Tokens shouldn't be stored unless the flag is set"
326        );
327        self.token_storage.push(StoredToken::new(
328            self.token.kind(),
329            self.token.source_range(),
330        ));
331    }
332
333    /// \return any stored comments to this point. Port of `getStoredComments`.
334    pub fn get_stored_comments(&self) -> &[StoredComment] {
335        &self.comment_storage
336    }
337
338    /// \return any stored comments to this point, moving them out of storage in
339    /// the lexer and clearing the storage. Port of `moveStoredComments`.
340    pub fn move_stored_comments(&mut self) -> Vec<StoredComment> {
341        std::mem::take(&mut self.comment_storage)
342    }
343
344    /// \return any stored tokens to this point. Port of `getStoredTokens`.
345    pub fn get_stored_tokens(&self) -> &[StoredToken] {
346        &self.token_storage
347    }
348
349    /// \return the source URL from the magic comment, or `None` if there was no
350    /// magic comment. Port of `getSourceURL`.
351    pub fn get_source_url(&self) -> Option<&str> {
352        self.source_url.as_deref()
353    }
354
355    /// \return the source mapping URL from the magic comment, or `None` if there
356    /// was no magic comment. Port of `getSourceMappingURL`.
357    pub fn get_source_mapping_url(&self) -> Option<&str> {
358        self.source_mapping_url.as_deref()
359    }
360
361    /// \return the end location of the previous token.
362    pub fn prev_token_end(&self) -> SMLoc {
363        self.prev_token_end
364    }
365
366    /// \return the current char pointer location. Port of `getCurLoc`
367    /// (JSLexer.h:567-569), which returns `SMLoc::getFromPointer(curCharPtr_)`;
368    /// the offset-based equivalent is the cursor's current location.
369    pub fn get_cur_loc(&self) -> SMLoc {
370        self.cur_loc()
371    }
372
373    /// \return the source buffer id we're currently parsing. Port of
374    /// `getBufferId` (JSLexer.h:704-706).
375    pub fn get_buffer_id(&self) -> SourceId {
376        self.buf_id
377    }
378
379    /// \return the SourceErrorManager. Port of `getSourceMgr` (JSLexer.h:516).
380    pub fn get_source_mgr(&self) -> &SourceErrorManager {
381        self.sm
382    }
383
384    /// \return a mutable reference to the SourceErrorManager so the parser can
385    /// report errors through the lexer. Mirrors the non-const `getSourceMgr()`
386    /// overload in `JSLexer.h:516` (C++ returns a non-const reference).
387    pub fn get_source_mgr_mut(&mut self) -> &mut SourceErrorManager {
388        self.sm
389    }
390
391    /// \return the string interner. Port of `getStringTable` (JSLexer.h:523).
392    /// (The C++ `getAllocator` has no Rust analog — the port uses the global
393    /// allocator and `AtomTable`'s own interning, so there is no bump allocator.)
394    pub fn get_string_table(&self) -> &AtomTable {
395        self.strtab
396    }
397
398    /// \return the logical bytes of the buffer (the source text without the
399    /// trailing NUL sentinel). Pointer->offset adaptation of `getBufferStart`/
400    /// `getBufferEnd` (JSLexer.h:709-716): C++ returns `bufferStart_`/
401    /// `bufferEnd_` pointers; the offset-based equivalent is the buffer byte
402    /// slice, with `get_buffer_start` == 0 and `get_buffer_end` == its length.
403    pub fn buffer_bytes(&self) -> &[u8] {
404        let raw = self.cursor.raw();
405        // `raw` includes the trailing NUL sentinel; drop it for the logical
406        // bytes (mirroring [bufferStart_, bufferEnd_)).
407        &raw[..raw.len() - 1]
408    }
409
410    /// \return the start offset of the buffer (always 0). Pointer->offset
411    /// adaptation of `getBufferStart` (JSLexer.h:708-711).
412    pub fn get_buffer_start(&self) -> u32 {
413        0
414    }
415
416    /// \return the end offset of the buffer (the logical byte length, excluding
417    /// the trailing NUL sentinel). Pointer->offset adaptation of `getBufferEnd`
418    /// (JSLexer.h:713-716).
419    pub fn get_buffer_end(&self) -> u32 {
420        self.buffer_bytes().len() as u32
421    }
422
423    /// For certain identifier-like syntactic forms, like Flow's
424    /// `renders? number`, we need to check that the `?` comes immediately after
425    /// `renders` with no whitespace. Port of `Token::checkFollowingCharacter`
426    /// (JSLexer.h:229-234), relocated from `Token` to `JSLexer`: our offset-based
427    /// `Token` has no buffer reference, so the check reads the buffer byte at the
428    /// current token's end offset.
429    ///
430    /// \return true iff the character directly after the current token matches
431    /// `c`. The next byte could be the EOF NUL or the start of a UTF-8 sequence,
432    /// but it is always present (the buffer is NUL-terminated, so the end offset
433    /// is always in-bounds).
434    pub fn check_following_character(&self, c: u8) -> bool {
435        debug_assert!(c < 128, "test character must be ASCII");
436        self.cursor.raw()[self.token.end_loc().offset as usize] == c
437    }
438
439    /// \return the source text `[start, end)` of the current token. Port of
440    /// `Token::inputStr` (JSLexer.h:136-140), relocated from `Token` to
441    /// `JSLexer`: our offset-based `Token` has no buffer reference, so the slice
442    /// is taken from the cursor's buffer.
443    pub fn token_input_str(&self) -> &[u8] {
444        self.cursor.slice(
445            self.token.start_loc().offset,
446            self.token.end_loc().offset,
447        )
448    }
449
450    /// Intern an identifier. Port of `getIdentifier(StringRef)`
451    /// (JSLexer.h:685-687).
452    pub fn get_identifier(&self, name: &[u8]) -> AtomBytes {
453        self.strtab.atom_bytes(name)
454    }
455
456    /// Convert the current token to an identifier-operator token. Port of
457    /// `convertCurTokenToIdentOp` (JSLexer.h:827-831).
458    /// \pre the current token is an identifier which is an IDENT_OP operator.
459    pub fn convert_cur_token_to_ident_op(&mut self, kind: TokenKind) {
460        debug_assert_eq!(self.token.kind(), TokenKind::identifier);
461        debug_assert_eq!(
462            self.strtab.bytes(self.token.get_identifier()),
463            crate::token_kinds::token_kind_str(kind).as_bytes()
464        );
465        self.token.set_ident_op(kind);
466    }
467
468    /// A location at the current cursor offset.
469    #[inline]
470    pub(crate) fn cur_loc(&self) -> SMLoc {
471        SMLoc {
472            source: self.buf_id,
473            offset: self.cursor.offset(),
474        }
475    }
476
477    /// Record the current cursor offset as the start of the current token.
478    #[inline]
479    fn set_token_start(&mut self) {
480        let loc = self.cur_loc();
481        self.token.set_start(loc);
482    }
483
484    /// Force an EOF at the next token. Port of `forceEOF`.
485    #[inline]
486    pub fn force_eof(&mut self) {
487        self.cursor.seek_end();
488    }
489
490    /// Move the lexer to the specified spot. Any future `advance` calls will
491    /// start from this position (the current token is not updated until such a
492    /// call). Port of `seek` (JSLexer.h:699-701).
493    #[inline]
494    pub fn seek(&mut self, loc: SMLoc) {
495        self.cursor.seek(loc.offset);
496    }
497
498    /// Emit an error at `loc` (Lexer subsystem). If the error limit was reached,
499    /// force EOF and return false; otherwise return true. Port of
500    /// `JSLexer::error(SMLoc, Twine)` (JSLexer.cpp:2497-2503).
501    pub(crate) fn error(&mut self, loc: SMLoc, msg: impl Into<String>) -> bool {
502        self.sm.error_at(loc, None, msg.into(), Subsystem::Lexer);
503        if !self.sm.is_error_limit_reached() {
504            return true;
505        }
506        self.force_eof();
507        false
508    }
509
510    /// Emit an error over `range` (Lexer subsystem). Port of
511    /// `JSLexer::error(SMRange, Twine)` (JSLexer.cpp:2505-2511).
512    pub(crate) fn error_range(&mut self, range: SMRange, msg: impl Into<String>) -> bool {
513        self.sm
514            .error_at(range.start, Some(range), msg.into(), Subsystem::Lexer);
515        if !self.sm.is_error_limit_reached() {
516            return true;
517        }
518        self.force_eof();
519        false
520    }
521
522    /// Finish a new token, setting the new token's end location and saving the
523    /// previous token's end location. Port of `finishToken` (JSLexer.h:1077).
524    #[inline]
525    fn finish_token(&mut self) {
526        self.prev_token_end = self.token.end_loc();
527        let end = self.cur_loc();
528        self.token.set_end(end);
529        if self.store_tokens {
530            self.store_current_token();
531        }
532    }
533
534    // ---- Punctuator helpers (port of the PUNC_* macros) ---------------------
535
536    /// `PUNC_L1_1`: single-char punctuator.
537    #[inline]
538    fn punc_l1_1(&mut self, tok: TokenKind) {
539        self.set_token_start();
540        self.token.set_punctuator(tok);
541        self.cursor.advance(1);
542    }
543
544    /// `PUNC_L2_2`: `ch1` -> `tok1`, `ch1 ch2` -> `tok2`.
545    #[inline]
546    fn punc_l2_2(&mut self, ch2: u8, tok1: TokenKind, tok2: TokenKind) {
547        self.set_token_start();
548        if self.cursor.peek_at(1) == ch2 {
549            self.token.set_punctuator(tok2);
550            self.cursor.advance(2);
551        } else {
552            self.token.set_punctuator(tok1);
553            self.cursor.advance(1);
554        }
555    }
556
557    /// `PUNC_L2_3`: `ch1`->`tok1`, `ch1 ch2a`->`tok2a`, `ch1 ch2b`->`tok2b`.
558    #[inline]
559    fn punc_l2_3(
560        &mut self,
561        ch2a: u8,
562        tok2a: TokenKind,
563        ch2b: u8,
564        tok2b: TokenKind,
565        tok1: TokenKind,
566    ) {
567        self.set_token_start();
568        let c1 = self.cursor.peek_at(1);
569        if c1 == ch2a {
570            self.token.set_punctuator(tok2a);
571            self.cursor.advance(2);
572        } else if c1 == ch2b {
573            self.token.set_punctuator(tok2b);
574            self.cursor.advance(2);
575        } else {
576            self.token.set_punctuator(tok1);
577            self.cursor.advance(1);
578        }
579    }
580
581    /// `PUNC_L3_3`: `ch1`->`tok1`, `ch1 ch2`->`tok2`, `ch1 ch2 ch3`->`tok3`.
582    #[inline]
583    fn punc_l3_3(
584        &mut self,
585        ch2: u8,
586        tok2: TokenKind,
587        ch3: u8,
588        tok3: TokenKind,
589        tok1: TokenKind,
590    ) {
591        self.set_token_start();
592        if self.cursor.peek_at(1) != ch2 {
593            self.token.set_punctuator(tok1);
594            self.cursor.advance(1);
595        } else if self.cursor.peek_at(2) == ch3 {
596            self.token.set_punctuator(tok3);
597            self.cursor.advance(3);
598        } else {
599            self.token.set_punctuator(tok2);
600            self.cursor.advance(2);
601        }
602    }
603
604    /// The non-ASCII `default:` arm of `advance` (JSLexer.cpp:711-735,
605    /// `default_label`). The cursor is on a non-ASCII byte. Decode the UTF-8
606    /// character and either scan a Unicode-only identifier, skip a Unicode-only
607    /// space, or report an unrecognized-character error.
608    ///
609    /// The C++ reaches this via `goto default_label` from the `c2`/`e2`/`ef`
610    /// lead-byte arms (when the bytes are not the recognized special sequence)
611    /// and from the `default:` case. We extract it into a helper so those arms
612    /// can call it (the faithful equivalent of the `goto`).
613    ///
614    /// \return `true` if the caller should `continue` the advance loop (the C++
615    ///   `continue` for a Unicode-only space or after an error), or `false` if
616    ///   the token is complete and the loop should `break` (the C++ `break`
617    ///   after scanning an identifier).
618    fn scan_default_non_ascii(&mut self, grammar_context: GrammarContext) -> bool {
619        self.set_token_start();
620        let ch = self.decode_utf8_advance();
621
622        if is_unicode_only_id_start(ch) {
623            self.tmp_storage.clear();
624            append_unicode_to_storage(&mut self.tmp_storage, ch);
625            self.scan_identifier_parts_in_context(grammar_context);
626            false
627        } else if is_unicode_only_space(ch) {
628            true
629        } else {
630            let range = SMRange {
631                start: self.token.start_loc(),
632                end: self.cur_loc(),
633            };
634            if ch > 31 && ch < 127 {
635                self.error_range(
636                    range,
637                    format!("unrecognized character '{}'", ch as u8 as char),
638                );
639            } else {
640                self.error_range(
641                    range,
642                    format!("unrecognized Unicode character \\u{:x}", ch),
643                );
644            }
645            true
646        }
647    }
648
649    /// Advance to the next token and return it. Port of `JSLexer::advance`
650    /// (JSLexer.cpp:255-745).
651    pub fn advance(&mut self, grammar_context: GrammarContext) -> &Token {
652        self.new_line_before_current_token = false;
653
654        loop {
655            // The cursor stays within the buffer (raw() includes the trailing NUL).
656            debug_assert!((self.cursor.offset() as usize) < self.cursor.raw().len());
657            let c = self.cursor.peek();
658            match c {
659                0 => {
660                    self.set_token_start();
661                    // Faithful to JSLexer.cpp case 0: both the at-EOF branch and the
662                    // post-error branch set EOF (clippy flags the duplicate arms).
663                    #[allow(clippy::if_same_then_else)]
664                    if self.cursor.at_end() {
665                        self.token.set_eof();
666                    } else if !self.error(self.token.start_loc(), "unrecognized Unicode character \\u0000")
667                    {
668                        self.token.set_eof();
669                    } else {
670                        self.cursor.advance(1);
671                        continue;
672                    }
673                }
674
675                // PUNC_L1_1 single-char punctuators.
676                b'}' => self.punc_l1_1(TokenKind::r_brace),
677                b'(' => self.punc_l1_1(TokenKind::l_paren),
678                b')' => self.punc_l1_1(TokenKind::r_paren),
679                b'[' => self.punc_l1_1(TokenKind::l_square),
680                b']' => self.punc_l1_1(TokenKind::r_square),
681                b';' => self.punc_l1_1(TokenKind::semi),
682                b',' => self.punc_l1_1(TokenKind::comma),
683                b'~' => self.punc_l1_1(TokenKind::tilde),
684                b':' => self.punc_l1_1(TokenKind::colon),
685
686                // { {|  (the `{|` form is Flow-only Type context)
687                b'{' => {
688                    self.set_token_start();
689                    if grammar_context == GrammarContext::Type
690                        && self.cursor.peek_at(1) == b'|'
691                    {
692                        self.token.set_punctuator(TokenKind::l_bracepipe);
693                        self.cursor.advance(2);
694                    } else {
695                        self.token.set_punctuator(TokenKind::l_brace);
696                        self.cursor.advance(1);
697                    }
698                }
699
700                // = => == ===
701                b'=' => {
702                    self.set_token_start();
703                    if self.cursor.peek_at(1) == b'>' {
704                        self.token.set_punctuator(TokenKind::equalgreater);
705                        self.cursor.advance(2);
706                    } else if self.cursor.peek_at(1) != b'=' {
707                        self.token.set_punctuator(TokenKind::equal);
708                        self.cursor.advance(1);
709                    } else if self.cursor.peek_at(2) == b'=' {
710                        self.token.set_punctuator(TokenKind::equalequalequal);
711                        self.cursor.advance(3);
712                    } else {
713                        self.token.set_punctuator(TokenKind::equalequal);
714                        self.cursor.advance(2);
715                    }
716                }
717
718                // ! != !==
719                b'!' => self.punc_l3_3(
720                    b'=',
721                    TokenKind::exclaimequal,
722                    b'=',
723                    TokenKind::exclaimequalequal,
724                    TokenKind::exclaim,
725                ),
726
727                // + ++ +=
728                b'+' => self.punc_l2_3(
729                    b'+',
730                    TokenKind::plusplus,
731                    b'=',
732                    TokenKind::plusequal,
733                    TokenKind::plus,
734                ),
735                // - -- -=
736                b'-' => self.punc_l2_3(
737                    b'-',
738                    TokenKind::minusminus,
739                    b'=',
740                    TokenKind::minusequal,
741                    TokenKind::minus,
742                ),
743
744                // & && &= &&=
745                b'&' => {
746                    self.set_token_start();
747                    if self.cursor.peek_at(1) == b'&' {
748                        if self.cursor.peek_at(2) == b'=' {
749                            self.token.set_punctuator(TokenKind::ampampequal);
750                            self.cursor.advance(3);
751                        } else {
752                            self.token.set_punctuator(TokenKind::ampamp);
753                            self.cursor.advance(2);
754                        }
755                    } else if self.cursor.peek_at(1) == b'=' {
756                        self.token.set_punctuator(TokenKind::ampequal);
757                        self.cursor.advance(2);
758                    } else {
759                        self.token.set_punctuator(TokenKind::amp);
760                        self.cursor.advance(1);
761                    }
762                }
763
764                // | || |= ||=  (the `|}` form is Flow-only Type context)
765                b'|' => {
766                    self.set_token_start();
767                    if grammar_context == GrammarContext::Type
768                        && self.cursor.peek_at(1) == b'}'
769                    {
770                        self.token.set_punctuator(TokenKind::piper_brace);
771                        self.cursor.advance(2);
772                    } else if self.cursor.peek_at(1) == b'|' {
773                        if self.cursor.peek_at(2) == b'=' {
774                            self.token.set_punctuator(TokenKind::pipepipeequal);
775                            self.cursor.advance(3);
776                        } else {
777                            self.token.set_punctuator(TokenKind::pipepipe);
778                            self.cursor.advance(2);
779                        }
780                    } else if self.cursor.peek_at(1) == b'=' {
781                        self.token.set_punctuator(TokenKind::pipeequal);
782                        self.cursor.advance(2);
783                    } else {
784                        self.token.set_punctuator(TokenKind::pipe);
785                        self.cursor.advance(1);
786                    }
787                }
788
789                // ? ?? ?. ??=
790                b'?' => {
791                    self.set_token_start();
792                    if self.cursor.peek_at(1) == b'.' && !is_ascii_digit(self.cursor.peek_at(2)) {
793                        // OptionalChainingPunctuator ::
794                        // ?. [lookahead does not contain DecimalDigit]
795                        // This is done to prevent `x?.3:y` from being recognized
796                        // as `x ?. 3 : y` instead of `x ? .3 : y`.
797                        self.token.set_punctuator(TokenKind::questiondot);
798                        self.cursor.advance(2);
799                    } else if self.cursor.peek_at(1) == b'?'
800                        && grammar_context != GrammarContext::Type
801                    {
802                        if self.cursor.peek_at(2) == b'=' {
803                            self.token.set_punctuator(TokenKind::questionquestionequal);
804                            self.cursor.advance(3);
805                        } else {
806                            self.token.set_punctuator(TokenKind::questionquestion);
807                            self.cursor.advance(2);
808                        }
809                    } else {
810                        self.token.set_punctuator(TokenKind::question);
811                        self.cursor.advance(1);
812                    }
813                }
814
815                // * *= ** **=
816                b'*' => {
817                    self.set_token_start();
818                    if self.cursor.peek_at(1) == b'=' {
819                        self.token.set_punctuator(TokenKind::starequal);
820                        self.cursor.advance(2);
821                    } else if self.cursor.peek_at(1) != b'*' {
822                        self.token.set_punctuator(TokenKind::star);
823                        self.cursor.advance(1);
824                    } else if self.cursor.peek_at(2) == b'=' {
825                        self.token.set_punctuator(TokenKind::starstarequal);
826                        self.cursor.advance(3);
827                    } else {
828                        self.token.set_punctuator(TokenKind::starstar);
829                        self.cursor.advance(2);
830                    }
831                }
832
833                // ^ ^=
834                b'^' => self.punc_l2_2(b'=', TokenKind::caret, TokenKind::caretequal),
835
836                // % %=  (the `%checks` form is Flow-only Type context)
837                b'%' => {
838                    self.set_token_start();
839                    let off = self.cursor.offset() as usize;
840                    let raw = self.cursor.raw();
841                    // `off + 7 < raw.len()` == C++ `curCharPtr_ + 7 <= bufferEnd_`
842                    // (raw includes the trailing NUL, so raw.len() - 1 is the NUL).
843                    if grammar_context == GrammarContext::Type
844                        && off + 7 < raw.len()
845                        && &raw[off..off + 7] == b"%checks"
846                    {
847                        // C++ routes this through getStringLiteral (faithful, though
848                        // `%checks` is pure ASCII so convertSurrogates is a no-op).
849                        let ident = self.get_string_literal(b"%checks");
850                        self.token.set_identifier(ident);
851                        self.cursor.advance(7);
852                    } else if self.cursor.peek_at(1) == b'=' {
853                        self.token.set_punctuator(TokenKind::percentequal);
854                        self.cursor.advance(2);
855                    } else {
856                        self.token.set_punctuator(TokenKind::percent);
857                        self.cursor.advance(1);
858                    }
859                }
860
861                // \r \n : line terminators set the newline flag.
862                b'\r' | b'\n' => {
863                    self.cursor.advance(1);
864                    self.new_line_before_current_token = true;
865                    continue;
866                }
867
868                // Line separator U+2028 (e2 80 a8) / Paragraph separator U+2029
869                // (e2 80 a9), or fall through to the default (non-ASCII) arm.
870                UTF8_LINE_TERMINATOR_CHAR0 => {
871                    if match_unicode_line_terminator_offset1(&self.cursor.raw()[self.cursor.offset() as usize..])
872                    {
873                        self.cursor.advance(3);
874                        self.new_line_before_current_token = true;
875                        continue;
876                    } else {
877                        // C++: goto default_label.
878                        if self.scan_default_non_ascii(grammar_context) {
879                            continue;
880                        }
881                    }
882                }
883
884                // \v \f : whitespace.
885                0x0b | 0x0c => {
886                    self.cursor.advance(1);
887                    continue;
888                }
889
890                // \t and space: tight loop to skip runs.
891                b'\t' | b' ' => {
892                    // Spaces frequently come in groups, so use a tight inner loop.
893                    loop {
894                        self.cursor.advance(1);
895                        let n = self.cursor.peek();
896                        if n != b'\t' && n != b' ' {
897                            break;
898                        }
899                    }
900                    continue;
901                }
902
903                // No-break space U+00A0 is UTF8 encoded as: c2 a0
904                0xc2 => {
905                    if self.cursor.peek_at(1) == 0xa0 {
906                        self.cursor.advance(2);
907                        continue;
908                    } else {
909                        // C++: goto default_label.
910                        if self.scan_default_non_ascii(grammar_context) {
911                            continue;
912                        }
913                    }
914                }
915
916                // Byte-order mark U+FEFF is encoded as: ef bb bf
917                0xef => {
918                    if self.cursor.peek_at(1) == 0xbb && self.cursor.peek_at(2) == 0xbf {
919                        self.cursor.advance(3);
920                        continue;
921                    } else {
922                        // C++: goto default_label.
923                        if self.scan_default_non_ascii(grammar_context) {
924                            continue;
925                        }
926                    }
927                }
928
929                // / // /* /=  and (in AllowRegExp) regexp.
930                b'/' => {
931                    if self.cursor.peek_at(1) == b'/' {
932                        // Line comment.
933                        self.scan_line_comment();
934                        continue;
935                    } else if self.cursor.peek_at(1) == b'*' {
936                        // Block comment.
937                        self.skip_block_comment();
938                        continue;
939                    } else {
940                        self.set_token_start();
941                        if grammar_context == GrammarContext::AllowRegExp {
942                            self.scan_regexp();
943                        } else if self.cursor.peek_at(1) == b'=' {
944                            self.token.set_punctuator(TokenKind::slashequal);
945                            self.cursor.advance(2);
946                        } else {
947                            self.token.set_punctuator(TokenKind::slash);
948                            self.cursor.advance(1);
949                        }
950                    }
951                }
952
953                // # : hashbang (only at buffer start) or private identifier.
954                b'#' => {
955                    if self.cursor.offset() == 0 && self.cursor.peek_at(1) == b'!' {
956                        // #! (hashbang) at the very start of the buffer.
957                        self.scan_line_comment();
958                        continue;
959                    }
960                    self.set_token_start();
961                    if !self.scan_private_identifier() {
962                        continue;
963                    }
964                }
965
966                // < <= << <<=  (in Type context, always `less`)
967                b'<' => {
968                    self.set_token_start();
969                    if grammar_context == GrammarContext::Type {
970                        self.token.set_punctuator(TokenKind::less);
971                        self.cursor.advance(1);
972                    } else if self.cursor.peek_at(1) == b'=' {
973                        self.token.set_punctuator(TokenKind::lessequal);
974                        self.cursor.advance(2);
975                    } else if self.cursor.peek_at(1) == b'<' {
976                        if self.cursor.peek_at(2) == b'=' {
977                            self.token.set_punctuator(TokenKind::lesslessequal);
978                            self.cursor.advance(3);
979                        } else {
980                            self.token.set_punctuator(TokenKind::lessless);
981                            self.cursor.advance(2);
982                        }
983                    } else {
984                        self.token.set_punctuator(TokenKind::less);
985                        self.cursor.advance(1);
986                    }
987                }
988
989                // > >= >> >>> >>= >>>=  (in Type/JSX context, always `greater`)
990                b'>' => {
991                    self.set_token_start();
992                    if grammar_context == GrammarContext::Type
993                        || grammar_context == GrammarContext::AllowJSXIdentifier
994                    {
995                        self.token.set_punctuator(TokenKind::greater);
996                        self.cursor.advance(1);
997                    } else if self.cursor.peek_at(1) == b'=' {
998                        // >=
999                        self.token.set_punctuator(TokenKind::greaterequal);
1000                        self.cursor.advance(2);
1001                    } else if self.cursor.peek_at(1) == b'>' {
1002                        // >>
1003                        if self.cursor.peek_at(2) == b'=' {
1004                            // >>=
1005                            self.token.set_punctuator(TokenKind::greatergreaterequal);
1006                            self.cursor.advance(3);
1007                        } else if self.cursor.peek_at(2) == b'>' {
1008                            // >>>
1009                            if self.cursor.peek_at(3) == b'=' {
1010                                // >>>=
1011                                self.token
1012                                    .set_punctuator(TokenKind::greatergreatergreaterequal);
1013                                self.cursor.advance(4);
1014                            } else {
1015                                self.token.set_punctuator(TokenKind::greatergreatergreater);
1016                                self.cursor.advance(3);
1017                            }
1018                        } else {
1019                            self.token.set_punctuator(TokenKind::greatergreater);
1020                            self.cursor.advance(2);
1021                        }
1022                    } else {
1023                        self.token.set_punctuator(TokenKind::greater);
1024                        self.cursor.advance(1);
1025                    }
1026                }
1027
1028                // . ... or .NNN (a number).
1029                b'.' => {
1030                    self.set_token_start();
1031                    if self.cursor.peek_at(1) >= b'0' && self.cursor.peek_at(1) <= b'9' {
1032                        self.scan_number(grammar_context);
1033                    } else if self.cursor.peek_at(1) == b'.' && self.cursor.peek_at(2) == b'.' {
1034                        self.token.set_punctuator(TokenKind::dotdotdot);
1035                        self.cursor.advance(3);
1036                    } else {
1037                        self.token.set_punctuator(TokenKind::period);
1038                        self.cursor.advance(1);
1039                    }
1040                }
1041
1042                // 0-9 : numbers.
1043                b'0'..=b'9' => {
1044                    self.set_token_start();
1045                    self.scan_number(grammar_context);
1046                }
1047
1048                // Identifier fast path.
1049                b'_' | b'$' | b'a'..=b'z' | b'A'..=b'Z' => {
1050                    self.set_token_start();
1051                    let start = self.cursor.offset();
1052                    self.scan_identifier_fast_path_in_context(start, grammar_context);
1053                }
1054
1055                // @ : decorator punctuator, or (in Flow Type context) the start
1056                // of an `@`-prefixed Flow identifier.
1057                b'@' => {
1058                    self.set_token_start();
1059                    if grammar_context == GrammarContext::Type {
1060                        let start = self.cursor.offset();
1061                        self.scan_identifier_fast_path_in_context(start, grammar_context);
1062                    } else {
1063                        self.token.set_punctuator(TokenKind::at);
1064                        self.cursor.advance(1);
1065                    }
1066                }
1067
1068                // \ : identifier with a leading unicode escape.
1069                // Port of JSLexer.cpp:683-698.
1070                b'\\' => {
1071                    self.set_token_start();
1072                    self.tmp_storage.clear();
1073                    let cp = self.consume_unicode_escape();
1074                    if !is_unicode_id_start(cp) {
1075                        self.error_range(
1076                            SMRange {
1077                                start: self.token.start_loc(),
1078                                end: self.cur_loc(),
1079                            },
1080                            format!(
1081                                "Unicode escape \\u{:x} is not a valid identifier start",
1082                                cp
1083                            ),
1084                        );
1085                        continue;
1086                    } else {
1087                        append_unicode_to_storage(&mut self.tmp_storage, cp);
1088                    }
1089                    self.scan_identifier_parts_in_context(grammar_context);
1090                }
1091
1092                // ' " : string literals.
1093                b'\'' | b'"' => {
1094                    self.set_token_start();
1095                    self.scan_string_in_context(grammar_context);
1096                }
1097
1098                // ` : template literal.
1099                b'`' => {
1100                    self.set_token_start();
1101                    self.scan_template_literal();
1102                }
1103
1104                // Default: non-ASCII identifier-start / unicode-only space /
1105                // unrecognized character. Port of JSLexer.cpp:711-735.
1106                _ => {
1107                    if self.scan_default_non_ascii(grammar_context) {
1108                        continue;
1109                    }
1110                }
1111            }
1112
1113            // Always terminate the loop unless "continue" was used.
1114            break;
1115        } // loop
1116
1117        self.finish_token();
1118        &self.token
1119    }
1120
1121    // ---- Comment scanners ---------------------------------------------------
1122
1123    /// Consume a line comment starting from the cursor (which is on `//` or
1124    /// `#!`) and return the comment offsets `(start, end)` EXCLUDING the line
1125    /// terminator. Update the cursor to point after the line terminator. Port of
1126    /// `lineCommentHelper` (JSLexer.cpp:1430-1480).
1127    fn line_comment_helper(&mut self) -> (u32, u32) {
1128        debug_assert!(
1129            (self.cursor.peek() == b'/' && self.cursor.peek_at(1) == b'/')
1130                || (self.cursor.peek() == b'#' && self.cursor.peek_at(1) == b'!')
1131        );
1132        let start = self.cursor.offset();
1133        // The end of the comment, excluding the line terminator.
1134        let line_comment_end;
1135        // Skip the two-character opening delimiter.
1136        self.cursor.advance(2);
1137
1138        loop {
1139            let c = self.cursor.peek();
1140            match c {
1141                0 => {
1142                    if self.cursor.at_end() {
1143                        line_comment_end = self.cursor.offset();
1144                        break;
1145                    } else {
1146                        self.cursor.advance(1);
1147                    }
1148                }
1149                b'\r' | b'\n' => {
1150                    line_comment_end = self.cursor.offset();
1151                    self.cursor.advance(1);
1152                    self.new_line_before_current_token = true;
1153                    break;
1154                }
1155                UTF8_LINE_TERMINATOR_CHAR0 => {
1156                    if match_unicode_line_terminator_offset1(
1157                        &self.cursor.raw()[self.cursor.offset() as usize..],
1158                    ) {
1159                        line_comment_end = self.cursor.offset();
1160                        self.cursor.advance(3);
1161                        self.new_line_before_current_token = true;
1162                        break;
1163                    } else {
1164                        self.decode_utf8_skip();
1165                    }
1166                }
1167                _ => {
1168                    if crate::utf8::is_utf8_start(c) {
1169                        self.decode_utf8_skip();
1170                    } else {
1171                        self.cursor.advance(1);
1172                    }
1173                }
1174            }
1175        }
1176
1177        (start, line_comment_end)
1178    }
1179
1180    /// Consume a line comment starting from the cursor (which is on `//` or
1181    /// `#!`). Optionally store the comment in comment storage. Update the cursor
1182    /// to point after the line terminator. Process magic comments. Port of
1183    /// `scanLineComment` (JSLexer.cpp:1482-1510).
1184    fn scan_line_comment(&mut self) {
1185        let first = self.cursor.peek();
1186        let (comment_start, comment_end) = self.line_comment_helper();
1187
1188        if self.store_comments {
1189            // `first == '/'` means a `//` line comment; otherwise `#!` hashbang.
1190            let kind = if first == b'/' {
1191                CommentKind::Line
1192            } else {
1193                CommentKind::Hashbang
1194            };
1195            self.comment_storage.push(StoredComment::new(
1196                kind,
1197                SMRange {
1198                    start: SMLoc {
1199                        source: self.buf_id,
1200                        offset: comment_start,
1201                    },
1202                    end: SMLoc {
1203                        source: self.buf_id,
1204                        offset: comment_end,
1205                    },
1206                },
1207            ));
1208        }
1209
1210        // Check for magic comments, which excludes #!.
1211        // Syntax is //# name=value
1212        let comment = self
1213            .cursor
1214            .slice(comment_start, comment_end)
1215            .to_vec();
1216        let Some(rest) = comment.strip_prefix(b"//# ") else {
1217            return;
1218        };
1219
1220        if let Some(value) = rest.strip_prefix(b"sourceURL=") {
1221            // The comment bytes point into the source buffer (ASCII-ish); store
1222            // them as a String for the lexer's own accessor and the manager.
1223            let value = String::from_utf8_lossy(value).into_owned();
1224            self.sm.set_source_url(self.buf_id, &value);
1225            self.source_url = Some(value);
1226        } else if let Some(value) = rest.strip_prefix(b"sourceMappingURL=") {
1227            let value = String::from_utf8_lossy(value).into_owned();
1228            self.sm.set_source_mapping_url(self.buf_id, &value);
1229            self.source_mapping_url = Some(value);
1230        }
1231    }
1232
1233    /// Skip a block comment (`/* ... */`), tracking the newline flag.
1234    /// Optionally store the comment in comment storage. Port of
1235    /// `skipBlockComment` (JSLexer.cpp:1512-1571). A non-terminated block comment
1236    /// reports an error + a "comment started here" note, matching the C++.
1237    fn skip_block_comment(&mut self) {
1238        debug_assert!(self.cursor.peek() == b'/' && self.cursor.peek_at(1) == b'*');
1239        let block_comment_start = self.cur_loc();
1240        // Skip the "/*" opening delimiter.
1241        self.cursor.advance(2);
1242
1243        loop {
1244            let c = self.cursor.peek();
1245            match c {
1246                0 => {
1247                    if self.cursor.at_end() {
1248                        let loc = self.cur_loc();
1249                        self.error(loc, "non-terminated block comment");
1250                        self.sm.note(block_comment_start, "comment started here");
1251                        break;
1252                    } else {
1253                        self.cursor.advance(1);
1254                    }
1255                }
1256                b'\r' | b'\n' => {
1257                    self.cursor.advance(1);
1258                    self.new_line_before_current_token = true;
1259                }
1260                UTF8_LINE_TERMINATOR_CHAR0 => {
1261                    if match_unicode_line_terminator_offset1(
1262                        &self.cursor.raw()[self.cursor.offset() as usize..],
1263                    ) {
1264                        self.cursor.advance(3);
1265                        self.new_line_before_current_token = true;
1266                    } else {
1267                        self.decode_utf8_skip();
1268                    }
1269                }
1270                b'*' => {
1271                    self.cursor.advance(1);
1272                    if self.cursor.peek() == b'/' {
1273                        self.cursor.advance(1);
1274                        break;
1275                    }
1276                }
1277                _ => {
1278                    if crate::utf8::is_utf8_start(c) {
1279                        self.decode_utf8_skip();
1280                    } else {
1281                        self.cursor.advance(1);
1282                    }
1283                }
1284            }
1285        }
1286
1287        if self.store_comments {
1288            self.comment_storage.push(StoredComment::new(
1289                CommentKind::Block,
1290                SMRange {
1291                    start: block_comment_start,
1292                    end: self.cur_loc(),
1293                },
1294            ));
1295        }
1296    }
1297
1298    /// Decode the UTF-8 sequence at the cursor, advance past it, and report any
1299    /// decode error at the start of the sequence. Port of the member
1300    /// `decodeUTF8` (JSLexer.h:1145-1151), which uses `decodeUTF8<false>`.
1301    pub(crate) fn decode_utf8_advance(&mut self) -> u32 {
1302        let save_start = self.cur_loc();
1303        let raw = self.cursor.raw();
1304        let mut i = self.cursor.offset() as usize;
1305        let mut err_msg: Option<String> = None;
1306        let cp = decode_utf8::<false>(raw, &mut i, |m| {
1307            if err_msg.is_none() {
1308                err_msg = Some(m.to_string());
1309            }
1310        });
1311        let consumed = (i - self.cursor.offset() as usize).max(1);
1312        self.cursor.advance(consumed);
1313        if let Some(msg) = err_msg {
1314            self.error(save_start, msg);
1315        }
1316        cp
1317    }
1318
1319    /// Decode the UTF-8 sequence at the cursor and advance past it, swallowing
1320    /// any decode errors. Mirrors the member `_decodeUTF8SlowPath(cur)` used
1321    /// inside the comment scanners (which advances the pointer; errors are
1322    /// reported via the member but we ignore them inside trivia for 1a).
1323    fn decode_utf8_skip(&mut self) {
1324        let raw = self.cursor.raw();
1325        let mut i = self.cursor.offset() as usize;
1326        let _ = decode_utf8::<true>(raw, &mut i, |_| {});
1327        // Advance the cursor by however many bytes were consumed (at least 1).
1328        let consumed = i - self.cursor.offset() as usize;
1329        self.cursor.advance(consumed.max(1));
1330    }
1331}
1332
1333/// \return true if `ch` is an ASCII decimal digit.
1334#[inline]
1335pub(crate) fn is_ascii_digit(ch: u8) -> bool {
1336    ch.is_ascii_digit()
1337}
1338
1339#[cfg(test)]
1340mod tests {
1341    use super::*;
1342    use hermes_atom_table::AtomTable;
1343    use hermes_support::manager::SourceErrorManager;
1344
1345    #[test]
1346    fn convert_surrogates() {
1347        // With convert_surrogates ON, an astral char in a string literal is
1348        // re-encoded to VALID UTF-8 (not the WTF-8 surrogate-pair form).
1349        let mut sm = SourceErrorManager::new();
1350        let id = sm.add_buffer("t", "'\\u{1F600}' '\\uD800'");
1351        let tab = AtomTable::new();
1352        let mut lex = JSLexer::new_with_convert_surrogates(
1353            id,
1354            &mut sm,
1355            &tab,
1356            GrammarContext::AllowDiv,
1357            true,
1358        );
1359        let t = lex.advance(GrammarContext::AllowDiv);
1360        assert_eq!(tab.bytes(t.get_string_literal()), b"\xf0\x9f\x98\x80"); // valid 4-byte UTF-8 emoji
1361        let t = lex.advance(GrammarContext::AllowDiv);
1362        assert_eq!(tab.bytes(t.get_string_literal()), "\u{FFFD}".as_bytes()); // lone surrogate -> U+FFFD
1363
1364        // With it OFF (default), the WTF-8 form is preserved (the existing 2a
1365        // behavior).
1366        let mut sm2 = SourceErrorManager::new();
1367        let id2 = sm2.add_buffer("t2", "'\\u{1F600}'");
1368        let tab2 = AtomTable::new();
1369        let mut lex2 =
1370            JSLexer::new(id2, &mut sm2, &tab2, GrammarContext::AllowDiv);
1371        let t = lex2.advance(GrammarContext::AllowDiv);
1372        assert_eq!(
1373            tab2.bytes(t.get_string_literal()),
1374            b"\xed\xa0\xbd\xed\xb8\x80"
1375        ); // WTF-8 surrogate pair
1376    }
1377
1378    /// Build a lexer over `src`, call `consume_unicode_escape` with the cursor
1379    /// on the leading `\`, and return the decoded code point unless an error was
1380    /// emitted (in which case `None`).
1381    fn consume_escape_for_test(src: &str) -> Option<u32> {
1382        let mut sm = SourceErrorManager::new();
1383        let id = sm.add_buffer("t", src);
1384        let tab = AtomTable::new();
1385        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1386        let cp = lex.consume_unicode_escape();
1387        if lex.sm.error_count() != 0 {
1388            None
1389        } else {
1390            Some(cp)
1391        }
1392    }
1393
1394    #[test]
1395    fn unicode_escape_4hex_and_braced() {
1396        assert_eq!(consume_escape_for_test("\\u0041"), Some(0x41)); // 'A'
1397        assert_eq!(consume_escape_for_test("\\u{1F600}"), Some(0x1F600));
1398        assert_eq!(consume_escape_for_test("\\u{}"), None); // empty -> error
1399        assert_eq!(consume_escape_for_test("\\uXY"), None); // bad hex -> error
1400    }
1401
1402    fn kinds(src: &str) -> Vec<TokenKind> {
1403        let mut sm = SourceErrorManager::new();
1404        let id = sm.add_buffer("t", src);
1405        let tab = AtomTable::new();
1406        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1407        let mut out = vec![];
1408        loop {
1409            let k = lex.advance(GrammarContext::AllowDiv).kind();
1410            out.push(k);
1411            if k == TokenKind::eof {
1412                break;
1413            }
1414        }
1415        out
1416    }
1417
1418    /// Like `kinds`, but lexes under an explicit grammar context (used for the
1419    /// Flow `Type`-context arms).
1420    fn kinds_ctx(src: &str, ctx: GrammarContext) -> Vec<TokenKind> {
1421        let mut sm = SourceErrorManager::new();
1422        let id = sm.add_buffer("t", src);
1423        let tab = AtomTable::new();
1424        let mut lex = JSLexer::new(id, &mut sm, &tab, ctx);
1425        let mut out = vec![];
1426        loop {
1427            let k = lex.advance(ctx).kind();
1428            out.push(k);
1429            if k == TokenKind::eof {
1430                break;
1431            }
1432        }
1433        out
1434    }
1435
1436    #[test]
1437    fn flow_type_context() {
1438        use TokenKind::*;
1439        assert_eq!(kinds_ctx("{|", GrammarContext::Type), vec![l_bracepipe, eof]);
1440        assert_eq!(kinds_ctx("|}", GrammarContext::Type), vec![piper_brace, eof]);
1441        // plain `{ }` still works in Type context.
1442        assert_eq!(
1443            kinds_ctx("{ }", GrammarContext::Type),
1444            vec![l_brace, r_brace, eof]
1445        );
1446        // `<` is `less` (not lessless etc.) in Type context.
1447        assert_eq!(kinds_ctx("<", GrammarContext::Type), vec![less, eof]);
1448        // `>>` lexes as two individual `>` in Type context.
1449        assert_eq!(
1450            kinds_ctx(">>", GrammarContext::Type),
1451            vec![greater, greater, eof]
1452        );
1453        // `??` is not formed in Type context (`?` is its own token).
1454        assert_eq!(
1455            kinds_ctx("??", GrammarContext::Type),
1456            vec![question, question, eof]
1457        );
1458        // `%checks` is an identifier in Type context.
1459        assert_eq!(kinds_ctx("%checks", GrammarContext::Type), vec![identifier, eof]);
1460        // `@`-prefixed Flow identifier.
1461        assert_eq!(kinds_ctx("@foo", GrammarContext::Type), vec![identifier, eof]);
1462        // Outside Type, these behave normally:
1463        assert_eq!(
1464            kinds_ctx("{|", GrammarContext::AllowDiv),
1465            vec![l_brace, pipe, eof]
1466        );
1467        assert_eq!(
1468            kinds_ctx("@foo", GrammarContext::AllowDiv),
1469            vec![at, identifier, eof]
1470        );
1471    }
1472
1473    /// Like `kinds`, but the lexer is switched to non-strict mode.
1474    fn kinds_nonstrict(src: &str) -> Vec<TokenKind> {
1475        let mut sm = SourceErrorManager::new();
1476        let id = sm.add_buffer("t", src);
1477        let tab = AtomTable::new();
1478        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1479        lex.set_strict_mode(false);
1480        let mut out = vec![];
1481        loop {
1482            let k = lex.advance(GrammarContext::AllowDiv).kind();
1483            out.push(k);
1484            if k == TokenKind::eof {
1485                break;
1486            }
1487        }
1488        out
1489    }
1490
1491    /// Lex `src` as a single identifier and return its interned bytes.
1492    fn ident_bytes(src: &str) -> Vec<u8> {
1493        let mut sm = SourceErrorManager::new();
1494        let id = sm.add_buffer("t", src);
1495        let tab = AtomTable::new();
1496        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1497        let tok = lex.advance(GrammarContext::AllowDiv);
1498        assert_eq!(tok.kind(), TokenKind::identifier);
1499        let ab = tok.get_identifier();
1500        tab.bytes(ab).to_vec()
1501    }
1502
1503    #[test]
1504    fn identifiers_and_reswords() {
1505        use TokenKind::*;
1506        assert_eq!(kinds("foo _bar $x9"), vec![identifier, identifier, identifier, eof]);
1507        assert_eq!(
1508            kinds("function for yield"),
1509            vec![rw_function, rw_for, rw_yield, eof]
1510        ); // strict mode default
1511           // non-strict: yield is an identifier
1512        assert_eq!(kinds_nonstrict("yield"), vec![identifier, eof]);
1513        // non-strict downgrade for the other future reserved words
1514        assert_eq!(
1515            kinds_nonstrict("implements interface package private protected public static"),
1516            vec![
1517                identifier, identifier, identifier, identifier, identifier, identifier,
1518                identifier, eof
1519            ]
1520        );
1521        // unicode identifier
1522        assert_eq!(kinds("\u{00e9}tude"), vec![identifier, eof]); // étude
1523                                                                  // escaped identifier start
1524        assert_eq!(kinds("\\u0041bc"), vec![identifier, eof]); // 'Abc'
1525                                                               // ident value round-trips through the interner
1526        assert_eq!(ident_bytes("caf\u{00e9}"), b"caf\xc3\xa9");
1527        // escaped identifier interns the decoded bytes
1528        assert_eq!(ident_bytes("\\u0041bc"), b"Abc");
1529    }
1530
1531    /// Lex `src` as a single numeric literal and return its f64 bits.
1532    fn num_bits(src: &str) -> u64 {
1533        let mut sm = SourceErrorManager::new();
1534        let id = sm.add_buffer("t", src);
1535        let tab = AtomTable::new();
1536        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1537        let tok = lex.advance(GrammarContext::AllowDiv);
1538        assert_eq!(tok.kind(), TokenKind::numeric_literal, "src={src:?}");
1539        tok.get_numeric_literal().to_bits()
1540    }
1541
1542    /// Lex `src` as a single bigint literal and return (value, raw) bytes.
1543    fn bigint_bytes(src: &str) -> (Vec<u8>, Vec<u8>) {
1544        let mut sm = SourceErrorManager::new();
1545        let id = sm.add_buffer("t", src);
1546        let tab = AtomTable::new();
1547        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1548        let tok = lex.advance(GrammarContext::AllowDiv);
1549        assert_eq!(tok.kind(), TokenKind::bigint_literal, "src={src:?}");
1550        let v = tab.bytes(tok.get_bigint_literal()).to_vec();
1551        let r = tab.bytes(tok.get_bigint_literal_raw_value()).to_vec();
1552        (v, r)
1553    }
1554
1555    #[test]
1556    fn numbers_basic() {
1557        use TokenKind::*;
1558        assert_eq!(num_bits("5"), 5.0f64.to_bits());
1559        assert_eq!(num_bits("0.1"), 0.1f64.to_bits());
1560        assert_eq!(num_bits("0xff"), 255.0f64.to_bits());
1561        assert_eq!(num_bits("0o17"), 15.0f64.to_bits());
1562        assert_eq!(num_bits("0b1010"), 10.0f64.to_bits());
1563        assert_eq!(num_bits("1e10"), 1e10f64.to_bits());
1564        assert_eq!(num_bits("1_000"), 1000.0f64.to_bits());
1565        assert_eq!(num_bits(".5"), 0.5f64.to_bits());
1566        assert_eq!(num_bits("3.14e2"), 314.0f64.to_bits());
1567        assert_eq!(num_bits("0XAB"), (0xab as f64).to_bits());
1568        assert_eq!(num_bits("0o7"), 7.0f64.to_bits());
1569        assert_eq!(num_bits("0b11"), 3.0f64.to_bits());
1570        assert_eq!(num_bits("2E-3"), 2e-3f64.to_bits());
1571        // kind check
1572        assert_eq!(
1573            kinds("5 0xff 1.5"),
1574            vec![numeric_literal, numeric_literal, numeric_literal, eof]
1575        );
1576    }
1577
1578    #[test]
1579    fn bigint_basic() {
1580        assert_eq!(bigint_bytes("10n"), (b"10".to_vec(), b"10n".to_vec()));
1581        assert_eq!(bigint_bytes("0xffn"), (b"0xff".to_vec(), b"0xffn".to_vec()));
1582        assert_eq!(bigint_bytes("255n"), (b"255".to_vec(), b"255n".to_vec()));
1583        assert_eq!(bigint_bytes("0n"), (b"0".to_vec(), b"0n".to_vec()));
1584        // separators stripped from value, kept in raw
1585        assert_eq!(
1586            bigint_bytes("1_000n"),
1587            (b"1000".to_vec(), b"1_000n".to_vec())
1588        );
1589    }
1590
1591    /// Lex `src` as a single string literal and return its (cooked bytes,
1592    /// contains_escapes) pair.
1593    fn str_cooked(src: &str) -> (Vec<u8>, bool) {
1594        let mut sm = SourceErrorManager::new();
1595        let id = sm.add_buffer("t", src);
1596        let tab = AtomTable::new();
1597        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1598        let tok = lex.advance(GrammarContext::AllowDiv);
1599        assert_eq!(tok.kind(), TokenKind::string_literal, "src={src:?}");
1600        let cooked = tab.bytes(tok.get_string_literal()).to_vec();
1601        let escapes = tok.get_string_literal_contains_escapes();
1602        (cooked, escapes)
1603    }
1604
1605    #[test]
1606    fn strings_basic() {
1607        use TokenKind::*;
1608        assert_eq!(kinds("'a' \"b\""), vec![string_literal, string_literal, eof]);
1609        assert_eq!(str_cooked("'hello'"), (b"hello".to_vec(), false));
1610        assert_eq!(str_cooked("\"a\\tb\""), (b"a\tb".to_vec(), true)); // \t -> tab, escapes=true
1611        assert_eq!(str_cooked("'\\n\\r\\\\'"), (vec![10, 13, b'\\'], true));
1612        assert_eq!(str_cooked("'\\x41'"), (b"A".to_vec(), true)); // \x41 -> 'A'
1613        assert_eq!(str_cooked("'\\u00e9'"), (b"\xc3\xa9".to_vec(), true)); // é (WTF-8)
1614        assert_eq!(str_cooked("'a\\\nb'"), (b"ab".to_vec(), true)); // escaped newline continuation
1615        assert_eq!(str_cooked("'caf\u{00e9}'"), (b"caf\xc3\xa9".to_vec(), false)); // raw unicode, no escape
1616    }
1617
1618    /// Lex `src` as a single private identifier and return its interned bytes.
1619    fn private_ident_bytes(src: &str) -> Vec<u8> {
1620        let mut sm = SourceErrorManager::new();
1621        let id = sm.add_buffer("t", src);
1622        let tab = AtomTable::new();
1623        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1624        let tok = lex.advance(GrammarContext::AllowDiv);
1625        assert_eq!(tok.kind(), TokenKind::private_identifier);
1626        tab.bytes(tok.get_private_identifier()).to_vec()
1627    }
1628
1629    /// Lex `src` as a single template literal token and return its
1630    /// `(kind, Option<cooked> bytes, raw bytes)`.
1631    fn template(src: &str) -> (TokenKind, Option<Vec<u8>>, Vec<u8>) {
1632        let mut sm = SourceErrorManager::new();
1633        let id = sm.add_buffer("t", src);
1634        let tab = AtomTable::new();
1635        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1636        let tok = lex.advance(GrammarContext::AllowDiv);
1637        assert!(tok.is_template_literal(), "src={src:?} kind={:?}", tok.kind());
1638        let kind = tok.kind();
1639        let cooked = tok.get_template_value().map(|a| tab.bytes(a).to_vec());
1640        let raw = tab.bytes(tok.get_template_raw_value()).to_vec();
1641        (kind, cooked, raw)
1642    }
1643
1644    #[test]
1645    fn templates_basic() {
1646        use TokenKind::*;
1647        // `abc` -> no_substitution_template, cooked="abc" raw="abc"
1648        assert_eq!(
1649            template("`abc`"),
1650            (no_substitution_template, Some(b"abc".to_vec()), b"abc".to_vec())
1651        );
1652        // `a${ -> template_head
1653        assert_eq!(template("`a${").0, template_head);
1654        // escapes: cooked has the cooked value, raw has the literal backslash seq
1655        assert_eq!(
1656            template("`a\\nb`"),
1657            (no_substitution_template, Some(vec![b'a', 10, b'b']), b"a\\nb".to_vec())
1658        );
1659        // NotEscapeSequence (\9) -> cooked is None, raw keeps it
1660        assert_eq!(
1661            template("`\\9`"),
1662            (no_substitution_template, None, b"\\9".to_vec())
1663        );
1664        // CR -> LF normalization in cooked AND raw
1665        assert_eq!(
1666            template("`a\rb`"),
1667            (no_substitution_template, Some(vec![b'a', 10, b'b']), vec![b'a', 10, b'b'])
1668        );
1669        // kind sequence: `a${ b } -> template_head, identifier, r_brace (rescan is
1670        // parser-driven, so the trailing ` starts a new — non-terminated — scan).
1671        assert_eq!(
1672            kinds("`a${b}")[..3].to_vec(),
1673            vec![template_head, identifier, r_brace]
1674        );
1675    }
1676
1677    #[test]
1678    fn private_identifiers() {
1679        use TokenKind::*;
1680        assert_eq!(
1681            kinds("#foo #bar"),
1682            vec![private_identifier, private_identifier, eof]
1683        );
1684        assert_eq!(private_ident_bytes("#x"), b"x"); // the interned name excludes '#'
1685                                                     // '#' followed by no identifier -> "empty private identifier"
1686                                                     // error; scan_private_identifier returns false -> no token.
1687        assert_eq!(kinds("#"), vec![eof]);
1688    }
1689
1690    #[test]
1691    fn punctuators_and_comments() {
1692        use TokenKind::*;
1693        assert_eq!(
1694            kinds("{ } ( ) ;"),
1695            vec![l_brace, r_brace, l_paren, r_paren, semi, eof]
1696        );
1697        // Comments and whitespace are skipped; only the `;` punctuators remain.
1698        assert_eq!(kinds("; /* c */ ;"), vec![semi, semi, eof]);
1699        assert_eq!(kinds("; // line\n;"), vec![semi, semi, eof]);
1700    }
1701
1702    #[test]
1703    fn token_storage() {
1704        // With store_tokens on, every advanced token (kind+range) is recorded.
1705        // finishToken stores the token it just finished, including the final
1706        // `eof` (advance scans eof, then finishToken records it). This matches
1707        // the C++ faithfully.
1708        let mut sm = SourceErrorManager::new();
1709        let id = sm.add_buffer("t", "a + b");
1710        let tab = AtomTable::new();
1711        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1712        lex.set_store_tokens(true);
1713        assert!(lex.get_store_tokens());
1714        while lex.advance(GrammarContext::AllowDiv).kind() != TokenKind::eof {}
1715        let toks: Vec<TokenKind> =
1716            lex.get_stored_tokens().iter().map(|t| t.kind()).collect();
1717        assert_eq!(
1718            toks,
1719            vec![
1720                TokenKind::identifier,
1721                TokenKind::plus,
1722                TokenKind::identifier,
1723                TokenKind::eof
1724            ]
1725        );
1726    }
1727
1728    #[test]
1729    fn comment_storage() {
1730        let mut sm = SourceErrorManager::new();
1731        let id = sm.add_buffer("t", "a /*c*/ // line\nb");
1732        let tab = AtomTable::new();
1733        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1734        lex.set_store_comments(true);
1735        while lex.advance(GrammarContext::AllowDiv).kind() != TokenKind::eof {}
1736        // Capture the comment ranges before re-borrowing the buffer for slicing.
1737        let cs: Vec<(CommentKind, u32, u32)> = lex
1738            .get_stored_comments()
1739            .iter()
1740            .map(|c| {
1741                let r = c.source_range();
1742                (c.kind(), r.start.offset, r.end.offset)
1743            })
1744            .collect();
1745        assert_eq!(cs.len(), 2);
1746        assert_eq!(cs[0].0, CommentKind::Block);
1747        assert_eq!(cs[1].0, CommentKind::Line);
1748        // The stored range includes the delimiters (getString strips them).
1749        let buf = sm.source_buffer(id);
1750        let raw = buf.raw();
1751        assert_eq!(&raw[cs[0].1 as usize..cs[0].2 as usize], b"/*c*/");
1752        assert_eq!(&raw[cs[1].1 as usize..cs[1].2 as usize], b"// line");
1753    }
1754
1755    #[test]
1756    fn magic_comments() {
1757        let mut sm = SourceErrorManager::new();
1758        let id = sm.add_buffer(
1759            "t",
1760            "a\n//# sourceURL=http://x/y.js\n//# sourceMappingURL=z.map\nb",
1761        );
1762        let tab = AtomTable::new();
1763        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1764        while lex.advance(GrammarContext::AllowDiv).kind() != TokenKind::eof {}
1765        assert_eq!(lex.get_source_url(), Some("http://x/y.js"));
1766        assert_eq!(lex.get_source_mapping_url(), Some("z.map"));
1767    }
1768
1769    #[test]
1770    fn save_point_restore() {
1771        let mut sm = SourceErrorManager::new();
1772        let id = sm.add_buffer("t", "a . b");
1773        let tab = AtomTable::new();
1774        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1775        lex.advance(GrammarContext::AllowDiv); // 'a' (identifier)
1776        let sp = lex.save_point();
1777        lex.advance(GrammarContext::AllowDiv); // '.'
1778        lex.advance(GrammarContext::AllowDiv); // 'b'
1779        sp.restore(&mut lex);
1780        // Current token is back to 'a'; next advance gives '.'.
1781        assert_eq!(lex.token().kind(), TokenKind::identifier);
1782        assert_eq!(
1783            lex.advance(GrammarContext::AllowDiv).kind(),
1784            TokenKind::period
1785        );
1786    }
1787
1788    #[test]
1789    fn save_point_truncates_storage() {
1790        // SavePoint restore truncates comment + token storage to the saved size.
1791        let mut sm = SourceErrorManager::new();
1792        let id = sm.add_buffer("t", "a /*c*/ . b");
1793        let tab = AtomTable::new();
1794        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1795        lex.set_store_tokens(true);
1796        lex.set_store_comments(true);
1797        lex.advance(GrammarContext::AllowDiv); // 'a'
1798        let toks_before = lex.get_stored_tokens().len();
1799        let comments_before = lex.get_stored_comments().len();
1800        let sp = lex.save_point();
1801        lex.advance(GrammarContext::AllowDiv); // '.', skips the /*c*/ comment
1802        lex.advance(GrammarContext::AllowDiv); // 'b'
1803        assert!(lex.get_stored_tokens().len() > toks_before);
1804        assert!(lex.get_stored_comments().len() > comments_before);
1805        sp.restore(&mut lex);
1806        assert_eq!(lex.get_stored_tokens().len(), toks_before);
1807        assert_eq!(lex.get_stored_comments().len(), comments_before);
1808    }
1809
1810    #[test]
1811    fn is_directive() {
1812        fn directive(src: &str) -> bool {
1813            let mut sm = SourceErrorManager::new();
1814            let id = sm.add_buffer("t", src);
1815            let tab = AtomTable::new();
1816            let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1817            lex.advance(GrammarContext::AllowDiv); // the string literal
1818            lex.is_current_token_a_directive()
1819        }
1820        assert!(directive("\"use strict\";"));
1821        assert!(directive("\"use strict\"\n"));
1822        assert!(directive("\"x\" /*c*/ ;"));
1823        assert!(directive("\"x\"")); // eof
1824        assert!(directive("\"x\" // line")); // line comment implies newline
1825        assert!(directive("\"x\" }")); // right brace
1826        assert!(!directive("\"x\" + y")); // followed by an operator
1827        assert!(!directive("foo")); // not a string literal
1828    }
1829
1830    #[test]
1831    fn is_directive_does_not_corrupt() {
1832        // After is_current_token_a_directive, the next advance is unaffected.
1833        let mut sm = SourceErrorManager::new();
1834        let id = sm.add_buffer("t", "\"x\" /*c*/ + y");
1835        let tab = AtomTable::new();
1836        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1837        assert_eq!(
1838            lex.advance(GrammarContext::AllowDiv).kind(),
1839            TokenKind::string_literal
1840        );
1841        assert!(!lex.is_current_token_a_directive());
1842        // The block comment is normally skipped; the next token is '+'.
1843        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), TokenKind::plus);
1844        assert_eq!(
1845            lex.advance(GrammarContext::AllowDiv).kind(),
1846            TokenKind::identifier
1847        );
1848    }
1849
1850    #[test]
1851    fn rescan_rbrace_template() {
1852        use TokenKind::*;
1853        // `a${b}c` : template_head, identifier(b), r_brace, then rescan ->
1854        // template_tail cooked="c".
1855        let mut sm = SourceErrorManager::new();
1856        let id = sm.add_buffer("t", "`a${b}c`");
1857        let tab = AtomTable::new();
1858        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1859        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), template_head);
1860        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), identifier);
1861        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), r_brace);
1862        let tok = lex.rescan_rbrace_in_template_literal();
1863        assert_eq!(tok.kind(), template_tail);
1864        let cooked = tok.get_template_value().map(|a| tab.bytes(a).to_vec());
1865        assert_eq!(cooked, Some(b"c".to_vec()));
1866    }
1867
1868    #[test]
1869    fn rescan_rbrace_template_middle() {
1870        use TokenKind::*;
1871        // `a${b}c${d}e` : the first rescan yields template_middle.
1872        let mut sm = SourceErrorManager::new();
1873        let id = sm.add_buffer("t", "`a${b}c${d}e`");
1874        let tab = AtomTable::new();
1875        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1876        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), template_head);
1877        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), identifier);
1878        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), r_brace);
1879        assert_eq!(
1880            lex.rescan_rbrace_in_template_literal().kind(),
1881            template_middle
1882        );
1883    }
1884
1885    #[test]
1886    fn newline_flag_tracks_line_terminators() {
1887        let mut sm = SourceErrorManager::new();
1888        let id = sm.add_buffer("t", ";\n;");
1889        let tab = AtomTable::new();
1890        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1891        // First `;` : no newline before it.
1892        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), TokenKind::semi);
1893        assert!(!lex.is_new_line_before_current_token());
1894        // Second `;` : preceded by a newline.
1895        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), TokenKind::semi);
1896        assert!(lex.is_new_line_before_current_token());
1897    }
1898
1899    /// Regression for the `scan_not_implemented` bug: a `c2`-led Unicode-only
1900    /// id-start (`ª` U+00AA = `c2 aa`) used to be routed to a "not yet
1901    /// implemented" stub that errored and forced EOF. It must now fall through
1902    /// to the default non-ASCII arm and lex as an identifier.
1903    #[test]
1904    fn unicode_only_id_start_via_c2_arm() {
1905        use TokenKind::*;
1906        assert_eq!(kinds("\u{00aa}"), vec![identifier, eof]);
1907    }
1908
1909    #[test]
1910    fn check_following_character() {
1911        let mut sm = SourceErrorManager::new();
1912        let id = sm.add_buffer("t", "renders?");
1913        let tab = AtomTable::new();
1914        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1915        // After scanning `renders`, the next character is `?`.
1916        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), TokenKind::identifier);
1917        assert!(lex.check_following_character(b'?'));
1918        assert!(!lex.check_following_character(b':'));
1919    }
1920
1921    #[test]
1922    fn token_input_str_returns_source_text() {
1923        let mut sm = SourceErrorManager::new();
1924        let id = sm.add_buffer("t", "  foobar  ");
1925        let tab = AtomTable::new();
1926        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1927        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), TokenKind::identifier);
1928        assert_eq!(lex.token_input_str(), b"foobar");
1929    }
1930
1931    #[test]
1932    fn convert_cur_token_to_ident_op_for_as() {
1933        use crate::token_kinds::token_kind_str;
1934        let mut sm = SourceErrorManager::new();
1935        let id = sm.add_buffer("t", "as");
1936        let tab = AtomTable::new();
1937        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1938        // `as` lexes as a plain identifier.
1939        assert_eq!(lex.advance(GrammarContext::AllowDiv).kind(), TokenKind::identifier);
1940        // Make sure the IDENT_OP kind we convert to actually has str == "as".
1941        assert_eq!(token_kind_str(TokenKind::as_operator), "as");
1942        lex.convert_cur_token_to_ident_op(TokenKind::as_operator);
1943        assert_eq!(lex.token().kind(), TokenKind::as_operator);
1944    }
1945
1946    #[test]
1947    fn get_identifier_and_buffer_id() {
1948        let mut sm = SourceErrorManager::new();
1949        let id = sm.add_buffer("t", "abc");
1950        let tab = AtomTable::new();
1951        let lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1952        // get_identifier interns into the shared table.
1953        let a = lex.get_identifier(b"hello");
1954        assert_eq!(tab.bytes(a), b"hello");
1955        // get_buffer_id returns the buffer we're lexing.
1956        assert_eq!(lex.get_buffer_id(), id);
1957        // buffer bytes exclude the trailing NUL sentinel.
1958        assert_eq!(lex.buffer_bytes(), b"abc");
1959        assert_eq!(lex.get_buffer_start(), 0);
1960        assert_eq!(lex.get_buffer_end(), 3);
1961    }
1962
1963    #[test]
1964    fn source_mgr_mut_reports_errors() {
1965        // Mirror the exact setup pattern used by other tests in this module:
1966        // SourceErrorManager::new(), sm.add_buffer, AtomTable::new(), JSLexer::new.
1967        let mut sm = SourceErrorManager::new();
1968        let id = sm.add_buffer("t", "x");
1969        let tab = AtomTable::new();
1970        let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
1971        let loc = lex.token().start_loc();
1972        lex.get_source_mgr_mut()
1973            .error_at(loc, None, "boom", Subsystem::Parser);
1974        assert_eq!(lex.get_source_mgr().error_count(), 1);
1975    }
1976}