Skip to main content

tsz_parser/parser/
state.rs

1//! Parser - Cache-optimized parser using `NodeArena`
2//!
3//! This parser uses the Node architecture (16 bytes per node vs 208 bytes)
4//! for 13x better cache locality. It produces the same AST semantically
5//! but stored in a more efficient format.
6//!
7//! # Architecture
8//!
9//! - Uses `NodeArena` instead of `NodeArena`
10//! - Each node is 16 bytes (vs 208 bytes for fat Node enum)
11//! - Node data is stored in separate typed pools
12//! - 4 nodes fit per 64-byte cache line (vs 0.31 for fat nodes)
13
14use tsz_common::diagnostics::diagnostic_codes;
15use tsz_common::limits::MAX_PARSER_RECURSION_DEPTH;
16
17use crate::parser::{
18    NodeIndex, NodeList,
19    node::{IdentifierData, NodeArena},
20    syntax_kind_ext,
21};
22use rustc_hash::FxHashMap;
23use tracing::trace;
24use tsz_common::interner::Atom;
25use tsz_scanner::scanner_impl::{ScannerState, TokenFlags};
26use tsz_scanner::{SyntaxKind, token_is_keyword};
27// =============================================================================
28// Parser Context Flags
29// =============================================================================
30
31/// Context flag: inside an async function/method/arrow
32pub const CONTEXT_FLAG_ASYNC: u32 = 1;
33/// Context flag: inside a generator function/method
34pub const CONTEXT_FLAG_GENERATOR: u32 = 2;
35/// Context flag: inside a static block (where 'await' is reserved)
36pub const CONTEXT_FLAG_STATIC_BLOCK: u32 = 4;
37/// Context flag: parsing a parameter default (where 'await' is not allowed)
38pub const CONTEXT_FLAG_PARAMETER_DEFAULT: u32 = 8;
39/// Context flag: disallow 'in' as a binary operator (for for-statement initializers)
40pub const CONTEXT_FLAG_DISALLOW_IN: u32 = 16;
41/// Context flag: parsing the `true` branch of a conditional expression.
42/// Suppresses type-annotated single-parameter arrow lookahead while
43/// that colon belongs to the surrounding conditional operator.
44pub const CONTEXT_FLAG_IN_CONDITIONAL_TRUE: u32 = 64;
45/// Context flag: parsing a class member name.
46pub const CONTEXT_FLAG_CLASS_MEMBER_NAME: u32 = 2048;
47/// Context flag: inside an ambient context (declare namespace/module)
48pub const CONTEXT_FLAG_AMBIENT: u32 = 32;
49/// Context flag: parsing a class body
50pub const CONTEXT_FLAG_IN_CLASS: u32 = 4096;
51/// Context flag: inside a decorator expression (@expr)
52/// When set, `[` should not be treated as element access (it starts a computed property name)
53pub const CONTEXT_FLAG_IN_DECORATOR: u32 = 128;
54/// Context flag: parsing parameters of a class constructor.
55pub const CONTEXT_FLAG_CONSTRUCTOR_PARAMETERS: u32 = 256;
56/// Context flag: parsing arrow function parameters.
57pub const CONTEXT_FLAG_ARROW_PARAMETERS: u32 = 512;
58/// Context flag: disallow conditional types (used inside `infer T extends X` constraint parsing).
59/// When set, `T extends U ? X : Y` is not parsed as a conditional type.
60pub const CONTEXT_FLAG_DISALLOW_CONDITIONAL_TYPES: u32 = 1024;
61/// Context flag: inside a block statement (function body, bare block, if/while/for body).
62/// When set, modifiers like `export` and `declare` are not allowed and emit TS1184.
63pub const CONTEXT_FLAG_IN_BLOCK: u32 = 8192;
64/// Context flag: parsing inside a parenthesized expression.
65/// Used to keep arrow-function/parenthesized recovery behavior consistent.
66pub const CONTEXT_FLAG_IN_PARENTHESIZED_EXPRESSION: u32 = 16384;
67
68// =============================================================================
69// Parse Diagnostic
70// =============================================================================
71
72/// A parse-time diagnostic (error or warning).
73#[derive(Clone, Debug)]
74pub struct ParseDiagnostic {
75    pub start: u32,
76    pub length: u32,
77    pub message: String,
78    pub code: u32,
79}
80
81pub struct IncrementalParseResult {
82    pub statements: NodeList,
83    pub end_pos: u32,
84    pub end_of_file_token: NodeIndex,
85    pub reparse_start: u32,
86}
87
88// =============================================================================
89// ParserState
90// =============================================================================
91
92/// A high-performance parser using Node architecture.
93///
94/// Error suppression distance in tokens
95///
96/// If we emitted an error within this distance, suppress subsequent errors
97/// to prevent cascading TS1005 and other noise errors.
98///
99/// This value was chosen empirically to match TypeScript's behavior:
100/// - Too small: Cascading errors aren't suppressed effectively
101/// - Too large: Genuine secondary errors are suppressed
102const ERROR_SUPPRESSION_DISTANCE: u32 = 3;
103
104/// This parser produces the same AST semantically as `ParserState`,
105/// but uses the cache-optimized `NodeArena` for storage.
106pub struct ParserState {
107    /// The scanner for tokenizing
108    pub(crate) scanner: ScannerState,
109    /// Arena for allocating Nodes
110    pub arena: NodeArena,
111    /// Source file name
112    pub(crate) file_name: String,
113    /// Parser context flags
114    pub context_flags: u32,
115    /// Current token
116    pub(crate) current_token: SyntaxKind,
117    /// List of parse diagnostics
118    pub(crate) parse_diagnostics: Vec<ParseDiagnostic>,
119    /// Node count for assigning IDs
120    pub(crate) node_count: u32,
121    /// Recursion depth for stack overflow protection
122    pub(crate) recursion_depth: u32,
123    /// Position of last error (to prevent cascading errors at same position)
124    pub(crate) last_error_pos: u32,
125    /// Stack of label scopes for duplicate label detection (TS1114)
126    /// Each scope is a map from label name to the position where it was first defined
127    pub(crate) label_scopes: Vec<FxHashMap<String, u32>>,
128}
129
130impl ParserState {
131    #[inline]
132    #[must_use]
133    pub(crate) fn u32_from_usize(&self, value: usize) -> u32 {
134        let _ = self;
135        u32::try_from(value).expect("parser offsets must fit in u32")
136    }
137
138    #[inline]
139    #[must_use]
140    pub(crate) fn u16_from_node_flags(&self, value: u32) -> u16 {
141        let _ = self;
142        u16::try_from(value).expect("parser node flags must fit in u16")
143    }
144
145    /// Create a new Parser for the given source text.
146    #[must_use]
147    pub fn new(file_name: String, source_text: String) -> Self {
148        let estimated_nodes = source_text.len() / 20; // Rough estimate
149        // Zero-copy: Pass source_text directly to scanner without cloning
150        // This eliminates the 2x memory overhead from duplicating the source
151        let scanner = ScannerState::new(source_text, true);
152        Self {
153            scanner,
154            arena: NodeArena::with_capacity(estimated_nodes),
155            file_name,
156            context_flags: 0,
157            current_token: SyntaxKind::Unknown,
158            parse_diagnostics: Vec::new(),
159            node_count: 0,
160            recursion_depth: 0,
161            last_error_pos: 0,
162            label_scopes: vec![FxHashMap::default()],
163        }
164    }
165
166    pub fn reset(&mut self, file_name: String, source_text: String) {
167        self.file_name = file_name;
168        self.scanner.set_text(source_text, None, None);
169        self.arena.clear();
170        self.context_flags = 0;
171        self.current_token = SyntaxKind::Unknown;
172        self.parse_diagnostics.clear();
173        self.node_count = 0;
174        self.recursion_depth = 0;
175        self.last_error_pos = 0;
176        self.label_scopes.clear();
177        self.label_scopes.push(FxHashMap::default());
178    }
179
180    /// Check recursion limit - returns true if we can continue, false if limit exceeded
181    pub(crate) fn enter_recursion(&mut self) -> bool {
182        if self.recursion_depth >= MAX_PARSER_RECURSION_DEPTH {
183            self.parse_error_at_current_token(
184                "Maximum recursion depth exceeded",
185                diagnostic_codes::UNEXPECTED_TOKEN,
186            );
187            false
188        } else {
189            self.recursion_depth += 1;
190            true
191        }
192    }
193
194    /// Centralized error suppression heuristic
195    ///
196    /// Prevents cascading errors by suppressing error reports if we've already
197    /// emitted an error recently (within `ERROR_SUPPRESSION_DISTANCE` tokens).
198    ///
199    /// This standardizes the inconsistency where:
200    /// - `parse_expected()` uses strict equality `!=`
201    /// - `parse_semicolon()` uses `abs_diff > 3`
202    ///
203    /// Returns true if we should report an error, false if we should suppress it
204    fn should_report_error(&self) -> bool {
205        // Always report first error
206        if self.last_error_pos == 0 {
207            return true;
208        }
209        let current = self.token_pos();
210        // Report if we've advanced past the suppression distance
211        // This prevents multiple errors for the same position while still
212        // catching genuine secondary errors
213        current.abs_diff(self.last_error_pos) > ERROR_SUPPRESSION_DISTANCE
214    }
215
216    /// Exit recursion scope
217    pub(crate) const fn exit_recursion(&mut self) {
218        self.recursion_depth = self.recursion_depth.saturating_sub(1);
219    }
220
221    // =========================================================================
222    // Token Utilities (shared with regular parser)
223    // =========================================================================
224
225    /// Check if we're in a JSX file.
226    /// In tsc, .js/.cjs/.mjs/.jsx/.tsx all use LanguageVariant.JSX,
227    /// only .ts/.cts/.mts use LanguageVariant.Standard.
228    pub(crate) fn is_jsx_file(&self) -> bool {
229        std::path::Path::new(&self.file_name)
230            .extension()
231            .and_then(|ext| ext.to_str())
232            .is_some_and(|ext| {
233                ext.eq_ignore_ascii_case("tsx")
234                    || ext.eq_ignore_ascii_case("jsx")
235                    || ext.eq_ignore_ascii_case("js")
236                    || ext.eq_ignore_ascii_case("cjs")
237                    || ext.eq_ignore_ascii_case("mjs")
238            })
239    }
240
241    /// Get current token
242    #[inline]
243    pub(crate) const fn token(&self) -> SyntaxKind {
244        self.current_token
245    }
246
247    /// Get current token position
248    #[inline]
249    pub(crate) fn token_pos(&self) -> u32 {
250        self.u32_from_usize(self.scanner.get_token_start())
251    }
252
253    /// Get current token end position
254    #[inline]
255    pub(crate) fn token_end(&self) -> u32 {
256        self.u32_from_usize(self.scanner.get_token_end())
257    }
258
259    /// Advance to next token
260    pub(crate) fn next_token(&mut self) -> SyntaxKind {
261        self.current_token = self.scanner.scan();
262        self.current_token
263    }
264
265    /// Consume a keyword token, checking for TS1260 (keywords cannot contain escape characters).
266    /// Call this instead of `next_token()` when consuming a keyword in a keyword position.
267    pub(crate) fn consume_keyword(&mut self) {
268        self.check_keyword_with_escape();
269        self.next_token();
270    }
271
272    /// Check if current token is a keyword with unicode escape and emit TS1260 if so.
273    /// Only call this when consuming a token that is expected to be a keyword.
274    fn check_keyword_with_escape(&mut self) {
275        // Skip if not a keyword
276        if !token_is_keyword(self.current_token) {
277            return;
278        }
279        // Check for UnicodeEscape flag
280        let flags = self.scanner.get_token_flags();
281        if (flags & TokenFlags::UnicodeEscape as u32) != 0 {
282            use tsz_common::diagnostics::diagnostic_codes;
283            self.parse_error_at(
284                self.u32_from_usize(self.scanner.get_token_start()),
285                self.u32_from_usize(self.scanner.get_token_end() - self.scanner.get_token_start()),
286                "Keywords cannot contain escape characters.",
287                diagnostic_codes::KEYWORDS_CANNOT_CONTAIN_ESCAPE_CHARACTERS,
288            );
289        }
290    }
291
292    /// Check if current token matches kind
293    #[inline]
294    pub(crate) fn is_token(&self, kind: SyntaxKind) -> bool {
295        self.current_token == kind
296    }
297
298    /// Check if current token is an identifier or any keyword
299    /// Keywords can be used as identifiers in many contexts (e.g., class names, property names)
300    #[inline]
301    pub(crate) const fn is_identifier_or_keyword(&self) -> bool {
302        self.current_token as u16 >= SyntaxKind::Identifier as u16
303    }
304
305    /// Check if current token can start a type member declaration
306    #[inline]
307    pub(crate) const fn is_type_member_start(&self) -> bool {
308        match self.current_token {
309            SyntaxKind::OpenParenToken | SyntaxKind::LessThanToken | SyntaxKind::NewKeyword => true,
310            _ => self.is_property_name(),
311        }
312    }
313
314    /// Check if current token can be a property name
315    /// Includes identifiers, keywords (as property names), string/numeric literals, computed properties
316    #[inline]
317    pub(crate) const fn is_property_name(&self) -> bool {
318        match self.current_token {
319            SyntaxKind::Identifier
320            | SyntaxKind::StringLiteral
321            | SyntaxKind::NumericLiteral
322            | SyntaxKind::PrivateIdentifier
323            | SyntaxKind::OpenBracketToken // computed property name
324            | SyntaxKind::GetKeyword
325            | SyntaxKind::SetKeyword => true,
326            // Any keyword can be used as a property name
327            _ => self.is_identifier_or_keyword()
328        }
329    }
330
331    /// Used to emit TS1110 (Type expected) instead of TS1005 (identifier expected)
332    /// when a type is expected but we encounter a token that can't start a type
333    #[inline]
334    pub(crate) const fn can_token_start_type(&self) -> bool {
335        match self.current_token {
336            // Tokens that definitely cannot start a type
337            SyntaxKind::CloseParenToken       // )
338            | SyntaxKind::CloseBraceToken     // }
339            | SyntaxKind::CloseBracketToken   // ]
340            | SyntaxKind::CommaToken          // ,
341            | SyntaxKind::SemicolonToken      // ;
342            | SyntaxKind::ColonToken          // :
343            | SyntaxKind::EqualsToken         // =
344            | SyntaxKind::EqualsGreaterThanToken  // =>
345            | SyntaxKind::GreaterThanToken    // > (e.g., missing type in generic default: T = >)
346            | SyntaxKind::BarToken            // | (when at start, not a union)
347            | SyntaxKind::AmpersandToken      // & (when at start, not an intersection)
348            | SyntaxKind::QuestionToken       // ?
349            | SyntaxKind::EndOfFileToken => false,
350            // Everything else could potentially start a type
351            // (identifiers, keywords, literals, type operators, etc.)
352            _ => true
353        }
354    }
355
356    /// Check if the current token is a delimiter/terminator where a missing type
357    /// should be silently recovered (no TS1110). TSC doesn't emit "Type expected" when
358    /// a type is simply omitted before a structural delimiter like `)`, `,`, `=>`, etc.
359    pub(crate) const fn is_type_terminator_token(&self) -> bool {
360        matches!(
361            self.current_token,
362            SyntaxKind::CloseParenToken          // ) - end of parameter list, parenthesized type
363            | SyntaxKind::CloseBracketToken      // ] - end of tuple/array type
364            | SyntaxKind::CloseBraceToken        // } - end of object type / block
365            | SyntaxKind::CommaToken             // , - next element in list
366            | SyntaxKind::SemicolonToken         // ; - end of statement
367            | SyntaxKind::EqualsGreaterThanToken // => - arrow (return type missing)
368            | SyntaxKind::EndOfFileToken // EOF
369        )
370    }
371
372    /// Parse type member separators with ASI-aware recovery.
373    ///
374    /// Type members in interface/type literal bodies allow:
375    /// - Explicit `;` or `,`
376    /// - ASI-separated members when a line break exists
377    ///
378    /// When members are missing a separator on the same line, emit
379    /// `';' expected.` (TS1005) and continue parsing.
380    pub(crate) fn parse_type_member_separator_with_asi(&mut self) {
381        if self.parse_optional(SyntaxKind::SemicolonToken)
382            || self.parse_optional(SyntaxKind::CommaToken)
383        {
384            return;
385        }
386
387        // No explicit separator and not at a boundary that permits implicit recovery.
388        if self.scanner.has_preceding_line_break() || self.is_token(SyntaxKind::CloseBraceToken) {
389            return;
390        }
391
392        if self.is_type_member_start() {
393            self.error_token_expected(";");
394        }
395    }
396
397    /// Check if we're inside an async function/method/arrow
398    #[inline]
399    pub(crate) const fn in_async_context(&self) -> bool {
400        (self.context_flags & CONTEXT_FLAG_ASYNC) != 0
401    }
402
403    /// Check if we're inside a generator function/method
404    #[inline]
405    pub(crate) const fn in_generator_context(&self) -> bool {
406        (self.context_flags & CONTEXT_FLAG_GENERATOR) != 0
407    }
408
409    /// Check if we're parsing a class member name.
410    #[inline]
411    pub(crate) const fn in_class_member_name(&self) -> bool {
412        (self.context_flags & CONTEXT_FLAG_CLASS_MEMBER_NAME) != 0
413    }
414
415    /// Check if we're parsing inside a class body.
416    #[inline]
417    pub(crate) const fn in_class_body(&self) -> bool {
418        (self.context_flags & CONTEXT_FLAG_IN_CLASS) != 0
419    }
420
421    /// Check if we're inside a static block
422    #[inline]
423    pub(crate) const fn in_static_block_context(&self) -> bool {
424        (self.context_flags & CONTEXT_FLAG_STATIC_BLOCK) != 0
425    }
426
427    /// Check if we're parsing a parameter default (where 'await' is not allowed)
428    #[inline]
429    pub(crate) const fn in_parameter_default_context(&self) -> bool {
430        (self.context_flags & CONTEXT_FLAG_PARAMETER_DEFAULT) != 0
431    }
432
433    /// Check if 'in' is disallowed as a binary operator (e.g., in for-statement initializers)
434    #[inline]
435    pub(crate) const fn in_disallow_in_context(&self) -> bool {
436        (self.context_flags & CONTEXT_FLAG_DISALLOW_IN) != 0
437    }
438
439    /// Check if we're inside a block statement (function body, bare block, etc.)
440    /// where modifiers like `export`/`declare` are not allowed.
441    #[inline]
442    pub(crate) const fn in_block_context(&self) -> bool {
443        (self.context_flags & CONTEXT_FLAG_IN_BLOCK) != 0
444    }
445
446    /// Check if we're currently parsing inside a parenthesized expression.
447    #[inline]
448    pub(crate) const fn in_parenthesized_expression_context(&self) -> bool {
449        (self.context_flags & CONTEXT_FLAG_IN_PARENTHESIZED_EXPRESSION) != 0
450    }
451
452    /// Check if the current token is an illegal binding identifier in the current context
453    /// Returns true if illegal and emits appropriate diagnostic
454    pub(crate) fn check_illegal_binding_identifier(&mut self) -> bool {
455        use tsz_common::diagnostics::diagnostic_codes;
456
457        // Check if current token is 'await' (either as keyword or identifier)
458        let is_await = self.is_token(SyntaxKind::AwaitKeyword)
459            || (self.is_token(SyntaxKind::Identifier)
460                && self.scanner.get_token_value_ref() == "await");
461
462        // Class members reject modifier-like keywords as computed property names.
463        // This emits TS1213 in class member context while leaving object/literal contexts unchanged.
464        if self.in_class_member_name()
465            && matches!(
466                self.token(),
467                SyntaxKind::PublicKeyword
468                    | SyntaxKind::PrivateKeyword
469                    | SyntaxKind::ProtectedKeyword
470                    | SyntaxKind::ReadonlyKeyword
471                    | SyntaxKind::StaticKeyword
472                    | SyntaxKind::AbstractKeyword
473                    | SyntaxKind::OverrideKeyword
474                    | SyntaxKind::AsyncKeyword
475                    | SyntaxKind::AwaitKeyword
476                    | SyntaxKind::YieldKeyword
477            )
478        {
479            let token_text = self.scanner.get_token_value_ref();
480            self.parse_error_at_current_token(
481                &format!(
482                    "Identifier expected. '{token_text}' is a reserved word in strict mode. Class definitions are automatically in strict mode."
483                ),
484                diagnostic_codes::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_IN_STRICT_MODE_CLASS_DEFINITIONS_ARE_AUTO,
485            );
486            return true;
487        }
488
489        if is_await {
490            // In static blocks, 'await' cannot be used as a binding identifier
491            if self.in_static_block_context() {
492                self.parse_error_at_current_token(
493                    "Identifier expected. 'await' is a reserved word that cannot be used here.",
494                    diagnostic_codes::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_THAT_CANNOT_BE_USED_HERE,
495                );
496                return true;
497            }
498
499            // In async contexts, 'await' cannot be used as a binding identifier
500            if self.in_async_context() {
501                self.parse_error_at_current_token(
502                    "Identifier expected. 'await' is a reserved word that cannot be used here.",
503                    diagnostic_codes::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_THAT_CANNOT_BE_USED_HERE,
504                );
505                return true;
506            }
507        }
508
509        // Check if current token is 'yield' (either as keyword or identifier)
510        // TS1359: 'yield' is a reserved word in generator functions
511        let is_yield = self.is_token(SyntaxKind::YieldKeyword)
512            || (self.is_token(SyntaxKind::Identifier)
513                && self.scanner.get_token_value_ref() == "yield");
514
515        if is_yield && self.in_generator_context() {
516            let is_class_context = self.in_class_body() || self.in_class_member_name();
517            if is_class_context {
518                self.parse_error_at_current_token(
519                    "Identifier expected. 'yield' is a reserved word in strict mode. Class definitions are automatically in strict mode.",
520                    diagnostic_codes::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_IN_STRICT_MODE_CLASS_DEFINITIONS_ARE_AUTO,
521                );
522            } else {
523                self.parse_error_at_current_token(
524                    "Identifier expected. 'yield' is a reserved word in strict mode.",
525                    diagnostic_codes::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_IN_STRICT_MODE,
526                );
527            }
528            return true;
529        }
530
531        false
532    }
533
534    /// Recover from invalid method/member syntax when `(` is missing after the member name.
535    /// This is used for async/generator forms like `async * get x() {}` where a single TS1005
536    /// should be emitted and the parser should skip the rest of the member to avoid cascades.
537    pub(crate) fn recover_from_missing_method_open_paren(&mut self) {
538        while !(self.is_token(SyntaxKind::OpenBraceToken)
539            || self.is_token(SyntaxKind::SemicolonToken)
540            || self.is_token(SyntaxKind::CommaToken)
541            || self.is_token(SyntaxKind::CloseBraceToken))
542        {
543            self.next_token();
544        }
545
546        if self.is_token(SyntaxKind::OpenBraceToken) {
547            let body = self.parse_block();
548            let _ = body;
549            return;
550        }
551
552        if self.is_token(SyntaxKind::SemicolonToken) || self.is_token(SyntaxKind::CommaToken) {
553            self.next_token();
554        }
555    }
556
557    /// Parse optional token, returns true if found
558    pub fn parse_optional(&mut self, kind: SyntaxKind) -> bool {
559        if self.is_token(kind) {
560            // Check for TS1260 if consuming a keyword
561            if token_is_keyword(kind) {
562                self.check_keyword_with_escape();
563            }
564            self.next_token();
565            true
566        } else {
567            false
568        }
569    }
570
571    /// Parse expected token, report error if not found
572    /// Suppresses error if we already emitted an error at the current position
573    /// (to prevent cascading errors from sequential `parse_expected` calls)
574    pub fn parse_expected(&mut self, kind: SyntaxKind) -> bool {
575        if self.is_token(kind) {
576            // Check for TS1260 if consuming a keyword
577            if token_is_keyword(kind) {
578                self.check_keyword_with_escape();
579            }
580            self.next_token();
581            true
582        } else if self.is_token(SyntaxKind::Unknown) {
583            // Unknown token = invalid character. In tsc, the scanner emits TS1127 via
584            // scanError callback and advances past the invalid char during scanning.
585            // The parser then sees the next real token. We replicate this by emitting
586            // TS1127, advancing, and re-checking for the expected token.
587            {
588                use tsz_common::diagnostics::diagnostic_codes;
589                self.parse_error_at_current_token(
590                    "Invalid character.",
591                    diagnostic_codes::INVALID_CHARACTER,
592                );
593            }
594            self.next_token();
595            // After skipping the invalid character, check if expected token is now present
596            if self.is_token(kind) {
597                self.next_token();
598                return true;
599            }
600            // Expected token still not found — emit TS1005 at the new position.
601            // In tsc, parseErrorAtPosition dedup is same-position only (not distance-based),
602            // so TS1005 at the post-Unknown position always emits. Use direct error emit
603            // to bypass our distance-based should_report_error() suppression.
604            {
605                use tsz_common::diagnostics::diagnostic_codes;
606                self.parse_error_at_current_token(
607                    &format!("'{}' expected.", Self::token_to_string(kind)),
608                    diagnostic_codes::EXPECTED,
609                );
610            }
611            false
612        } else {
613            // Force error emission for missing ) in common patterns.
614            // This bypasses the should_report_error() distance check.
615            let force_emit = kind == SyntaxKind::CloseParenToken
616                && (self.is_token(SyntaxKind::OpenBraceToken)
617                    || self.is_token(SyntaxKind::CloseBraceToken)
618                    || self.is_token(SyntaxKind::EndOfFileToken));
619
620            // Only emit error if we haven't already emitted one at this position
621            // This prevents cascading errors like "';' expected" followed by "')' expected"
622            // when the real issue is a single missing token
623            // Use centralized error suppression heuristic
624            if force_emit || self.should_report_error() {
625                // Additional check: suppress error for missing closing tokens when we're
626                // at a clear statement boundary or EOF (reduces false-positive TS1005 errors)
627                let should_suppress = if force_emit {
628                    false // Never suppress forced errors
629                } else {
630                    match kind {
631                        SyntaxKind::CloseBraceToken | SyntaxKind::CloseBracketToken => {
632                            // At EOF, the file ended before this closing token. TypeScript reports
633                            // these missing closing delimiters, so do not suppress at EOF.
634                            if self.is_token(SyntaxKind::EndOfFileToken) {
635                                false
636                            }
637                            // If next token starts a statement, the user has clearly moved on
638                            // Don't complain about missing closing token
639                            else if self.is_statement_start() {
640                                true
641                            }
642                            // If there's a line break, give the user benefit of doubt
643                            else {
644                                self.scanner.has_preceding_line_break()
645                            }
646                        }
647                        SyntaxKind::CloseParenToken => {
648                            // Missing ) is almost always a genuine error — don't suppress
649                            // at EOF, statement boundaries, or block delimiters.
650                            // Only suppress if on same line with no clear boundary.
651                            if self.is_token(SyntaxKind::EndOfFileToken) {
652                                false
653                            } else if self.scanner.has_preceding_line_break() {
654                                // At a line break, suppress unless it's a clear boundary
655                                !self.is_statement_start()
656                                    && !self.is_token(SyntaxKind::CloseBraceToken)
657                            } else {
658                                false
659                            }
660                        }
661                        _ => false,
662                    }
663                };
664
665                if !should_suppress {
666                    // For forced errors, bypass the normal error budget logic
667                    if force_emit {
668                        use tsz_common::diagnostics::diagnostic_codes;
669                        self.parse_error_at_current_token(
670                            &format!("'{}' expected.", Self::token_to_string(kind)),
671                            diagnostic_codes::EXPECTED,
672                        );
673                    } else {
674                        self.error_token_expected(Self::token_to_string(kind));
675                    }
676                }
677            }
678            false
679        }
680    }
681
682    /// Convert `SyntaxKind` to human-readable token string
683    pub(crate) const fn token_to_string(kind: SyntaxKind) -> &'static str {
684        match kind {
685            SyntaxKind::OpenBraceToken => "{",
686            SyntaxKind::CloseBraceToken => "}",
687            SyntaxKind::OpenParenToken => "(",
688            SyntaxKind::CloseParenToken => ")",
689            SyntaxKind::OpenBracketToken => "[",
690            SyntaxKind::CloseBracketToken => "]",
691            SyntaxKind::SemicolonToken => ";",
692            SyntaxKind::CommaToken => ",",
693            SyntaxKind::ColonToken => ":",
694            SyntaxKind::DotToken => ".",
695            SyntaxKind::EqualsToken => "=",
696            SyntaxKind::GreaterThanToken => ">",
697            SyntaxKind::LessThanToken => "<",
698            SyntaxKind::QuestionToken => "?",
699            SyntaxKind::ExclamationToken => "!",
700            SyntaxKind::AtToken => "@",
701            SyntaxKind::AmpersandToken => "&",
702            SyntaxKind::BarToken => "|",
703            SyntaxKind::PlusToken => "+",
704            SyntaxKind::MinusToken => "-",
705            SyntaxKind::AsteriskToken => "*",
706            SyntaxKind::SlashToken => "/",
707            SyntaxKind::EqualsGreaterThanToken => "=>",
708            SyntaxKind::DotDotDotToken => "...",
709            SyntaxKind::Identifier => "identifier",
710            SyntaxKind::TryKeyword => "try",
711            _ => "token",
712        }
713    }
714
715    pub(crate) fn parse_error_at(&mut self, start: u32, length: u32, message: &str, code: u32) {
716        // Don't report another error if it would just be at the same position as the last error.
717        // This matches tsc's parseErrorAtPosition deduplication behavior where parser errors
718        // at the same position are suppressed (only the first one survives).
719        if let Some(last) = self.parse_diagnostics.last()
720            && last.start == start
721        {
722            return;
723        }
724        // Track the position of this error to prevent cascading errors at same position
725        self.last_error_pos = start;
726        self.parse_diagnostics.push(ParseDiagnostic {
727            start,
728            length,
729            message: message.to_string(),
730            code,
731        });
732    }
733
734    /// Report parse error at current token with specific error code
735    pub fn parse_error_at_current_token(&mut self, message: &str, code: u32) {
736        let start = self.u32_from_usize(self.scanner.get_token_start());
737        let end = self.u32_from_usize(self.scanner.get_token_end());
738        self.parse_error_at(start, end - start, message, code);
739    }
740    pub(crate) fn report_invalid_string_or_template_escape_errors(&mut self) {
741        let token_text = self.scanner.get_token_text_ref().to_string();
742        if token_text.is_empty()
743            || (self.scanner.get_token_flags() & TokenFlags::ContainsInvalidEscape as u32) == 0
744        {
745            return;
746        }
747
748        let bytes = token_text.as_bytes();
749        let token_len = bytes.len();
750        let token_start = self.token_pos() as usize;
751
752        let Some((content_start_offset, content_end_offset)) =
753            self.string_template_escape_content_span(token_len, bytes)
754        else {
755            return;
756        };
757
758        if content_end_offset <= content_start_offset || content_end_offset > token_len {
759            return;
760        }
761
762        let raw = &bytes[content_start_offset..content_end_offset];
763        let content_start = token_start + content_start_offset;
764
765        let mut i = 0usize;
766
767        while i < raw.len() {
768            if raw[i] != b'\\' {
769                i += 1;
770                continue;
771            }
772            if i + 1 >= raw.len() {
773                break;
774            }
775            i = match raw[i + 1] {
776                b'x' => self.report_invalid_string_or_template_hex_escape(raw, content_start, i),
777                b'u' => {
778                    self.report_invalid_string_or_template_unicode_escape(raw, content_start, i)
779                }
780                _ => i + 1,
781            };
782        }
783    }
784
785    fn string_template_escape_content_span(
786        &self,
787        token_len: usize,
788        bytes: &[u8],
789    ) -> Option<(usize, usize)> {
790        match self.current_token {
791            SyntaxKind::StringLiteral => {
792                if token_len < 2
793                    || (bytes[0] != b'"' && bytes[0] != b'\'')
794                    || bytes[token_len - 1] != bytes[0]
795                {
796                    return None;
797                }
798                Some((1, token_len - 1))
799            }
800            SyntaxKind::NoSubstitutionTemplateLiteral => {
801                if bytes[0] != b'`' || bytes[token_len - 1] != b'`' {
802                    return None;
803                }
804                Some((1, token_len - 1))
805            }
806            SyntaxKind::TemplateHead => {
807                if bytes[0] != b'`' || !bytes.ends_with(b"${") {
808                    return None;
809                }
810                Some((1, token_len - 2))
811            }
812            SyntaxKind::TemplateMiddle | SyntaxKind::TemplateTail => {
813                if bytes[0] != b'}' {
814                    return None;
815                }
816                if bytes.ends_with(b"${") {
817                    Some((1, token_len - 2))
818                } else if bytes.ends_with(b"`") {
819                    Some((1, token_len - 1))
820                } else {
821                    Some((1, token_len))
822                }
823            }
824            _ => None,
825        }
826    }
827
828    fn report_invalid_string_or_template_hex_escape(
829        &mut self,
830        raw: &[u8],
831        content_start: usize,
832        i: usize,
833    ) -> usize {
834        use tsz_common::diagnostics::{diagnostic_codes, diagnostic_messages};
835
836        let first = i + 2;
837        let second = i + 3;
838        let err_len = |offset: usize| u32::from(offset < raw.len());
839
840        if first >= raw.len() || !Self::is_hex_digit(raw[first]) {
841            self.parse_error_at(
842                self.u32_from_usize(content_start + first),
843                err_len(first),
844                diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
845                diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
846            );
847        } else if second >= raw.len() || !Self::is_hex_digit(raw[second]) {
848            self.parse_error_at(
849                self.u32_from_usize(content_start + second),
850                err_len(second),
851                diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
852                diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
853            );
854        }
855        i + 2
856    }
857
858    fn report_invalid_string_or_template_unicode_escape(
859        &mut self,
860        raw: &[u8],
861        content_start: usize,
862        i: usize,
863    ) -> usize {
864        use tsz_common::diagnostics::{diagnostic_codes, diagnostic_messages};
865
866        if i + 2 >= raw.len() {
867            self.parse_error_at(
868                self.u32_from_usize(content_start + i + 2),
869                u32::from(i + 2 < raw.len()),
870                diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
871                diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
872            );
873            return i + 2;
874        }
875
876        if raw[i + 2] == b'{' {
877            let mut close = i + 3;
878            let mut has_digit = false;
879            while close < raw.len() && Self::is_hex_digit(raw[close]) {
880                has_digit = true;
881                close += 1;
882            }
883            if close >= raw.len() {
884                if !has_digit {
885                    // No hex digits at all: \u{ followed by end → TS1125
886                    self.parse_error_at(
887                        self.u32_from_usize(content_start + i + 3),
888                        0,
889                        diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
890                        diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
891                    );
892                } else {
893                    // Had hex digits but no closing brace → TS1508
894                    self.parse_error_at(
895                        self.u32_from_usize(content_start + close),
896                        0,
897                        diagnostic_messages::UNTERMINATED_UNICODE_ESCAPE_SEQUENCE,
898                        diagnostic_codes::UNTERMINATED_UNICODE_ESCAPE_SEQUENCE,
899                    );
900                }
901            } else if raw[close] == b'}' {
902                if !has_digit {
903                    self.parse_error_at(
904                        self.u32_from_usize(content_start + close),
905                        1,
906                        diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
907                        diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
908                    );
909                } else {
910                    // Check if the value exceeds 0x10FFFF (TS1198)
911                    let hex_str = std::str::from_utf8(&raw[i + 3..close]).unwrap_or("");
912                    if let Ok(value) = u32::from_str_radix(hex_str, 16)
913                        && value > 0x10FFFF
914                    {
915                        self.parse_error_at(
916                            self.u32_from_usize(content_start + i),
917                            (close + 1 - i) as u32,
918                            diagnostic_messages::AN_EXTENDED_UNICODE_ESCAPE_VALUE_MUST_BE_BETWEEN_0X0_AND_0X10FFFF_INCLUSIVE,
919                            diagnostic_codes::AN_EXTENDED_UNICODE_ESCAPE_VALUE_MUST_BE_BETWEEN_0X0_AND_0X10FFFF_INCLUSIVE,
920                        );
921                    }
922                }
923            } else if !has_digit {
924                self.parse_error_at(
925                    self.u32_from_usize(content_start + i + 3),
926                    1,
927                    diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
928                    diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
929                );
930            } else {
931                self.parse_error_at(
932                    self.u32_from_usize(content_start + close),
933                    1,
934                    diagnostic_messages::UNTERMINATED_UNICODE_ESCAPE_SEQUENCE,
935                    diagnostic_codes::UNTERMINATED_UNICODE_ESCAPE_SEQUENCE,
936                );
937            }
938            close + 1
939        } else {
940            let first = i + 2;
941            let second = i + 3;
942            let third = i + 4;
943            let fourth = i + 5;
944            let err_len = |offset: usize| u32::from(offset < raw.len());
945
946            if first >= raw.len() || !Self::is_hex_digit(raw[first]) {
947                self.parse_error_at(
948                    self.u32_from_usize(content_start + first),
949                    err_len(first),
950                    diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
951                    diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
952                );
953            } else if second >= raw.len() || !Self::is_hex_digit(raw[second]) {
954                self.parse_error_at(
955                    self.u32_from_usize(content_start + second),
956                    err_len(second),
957                    diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
958                    diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
959                );
960            } else if third >= raw.len() || !Self::is_hex_digit(raw[third]) {
961                self.parse_error_at(
962                    self.u32_from_usize(content_start + third),
963                    err_len(third),
964                    diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
965                    diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
966                );
967            } else if fourth >= raw.len() || !Self::is_hex_digit(raw[fourth]) {
968                self.parse_error_at(
969                    self.u32_from_usize(content_start + fourth),
970                    err_len(fourth),
971                    diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
972                    diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
973                );
974            }
975            i + 2
976        }
977    }
978
979    #[inline]
980    const fn is_hex_digit(byte: u8) -> bool {
981        byte.is_ascii_hexdigit()
982    }
983
984    /// Parse regex unicode escape diagnostics for regex literals in /u or /v mode.
985    pub(crate) fn report_invalid_regular_expression_escape_errors(&mut self) {
986        let token_text = self.scanner.get_token_text_ref().to_string();
987        if !token_text.starts_with('/') || token_text.len() < 2 {
988            return;
989        }
990
991        let bytes = token_text.as_bytes();
992        let mut i = 1usize;
993        let mut in_escape = false;
994        let mut in_character_class = false;
995        while i < bytes.len() {
996            let ch = bytes[i];
997            if in_escape {
998                in_escape = false;
999                i += 1;
1000                continue;
1001            }
1002            if ch == b'\\' {
1003                in_escape = true;
1004                i += 1;
1005                continue;
1006            }
1007            if ch == b'[' {
1008                in_character_class = true;
1009                i += 1;
1010                continue;
1011            }
1012            if ch == b']' {
1013                in_character_class = false;
1014                i += 1;
1015                continue;
1016            }
1017            if ch == b'/' && !in_character_class {
1018                break;
1019            }
1020            i += 1;
1021        }
1022        if i >= bytes.len() {
1023            return;
1024        }
1025
1026        let body = &token_text[1..i];
1027        let flags = if i + 1 < token_text.len() {
1028            &token_text[i + 1..]
1029        } else {
1030            ""
1031        };
1032        let has_unicode_flag = flags.as_bytes().iter().any(|&b| b == b'u' || b == b'v');
1033        if !has_unicode_flag {
1034            return;
1035        }
1036
1037        let body_start = self.token_pos() as usize + 1;
1038        let raw = body.as_bytes();
1039        let mut j = 0usize;
1040
1041        while j < raw.len() {
1042            if raw[j] != b'\\' {
1043                j += 1;
1044                continue;
1045            }
1046            if j + 1 >= raw.len() {
1047                break;
1048            }
1049            match raw[j + 1] {
1050                b'x' => {
1051                    j = self.report_invalid_regular_expression_hex_escape(raw, body_start, j);
1052                }
1053                b'u' => {
1054                    if let Some(next) =
1055                        self.report_invalid_regular_expression_unicode_escape(raw, body_start, j)
1056                    {
1057                        j = next;
1058                    } else {
1059                        break;
1060                    }
1061                }
1062                _ => {
1063                    j += 1;
1064                }
1065            }
1066        }
1067    }
1068
1069    fn report_invalid_regular_expression_hex_escape(
1070        &mut self,
1071        raw: &[u8],
1072        body_start: usize,
1073        j: usize,
1074    ) -> usize {
1075        use tsz_common::diagnostics::{diagnostic_codes, diagnostic_messages};
1076
1077        let first = j + 2;
1078        let second = j + 3;
1079        if first >= raw.len() || !Self::is_hex_digit(raw[first]) {
1080            self.parse_error_at(
1081                self.u32_from_usize(body_start + first),
1082                u32::from(first < raw.len()),
1083                diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
1084                diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
1085            );
1086        } else if second >= raw.len() || !Self::is_hex_digit(raw[second]) {
1087            self.parse_error_at(
1088                self.u32_from_usize(body_start + second),
1089                u32::from(second < raw.len()),
1090                diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
1091                diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
1092            );
1093        }
1094        j + 2
1095    }
1096
1097    fn report_invalid_regular_expression_unicode_escape(
1098        &mut self,
1099        raw: &[u8],
1100        body_start: usize,
1101        j: usize,
1102    ) -> Option<usize> {
1103        use tsz_common::diagnostics::{diagnostic_codes, diagnostic_messages};
1104
1105        if j + 2 < raw.len() && raw[j + 2] == b'{' {
1106            let mut close = j + 3;
1107            let mut has_digit = false;
1108            while close < raw.len() && Self::is_hex_digit(raw[close]) {
1109                has_digit = true;
1110                close += 1;
1111            }
1112            if close >= raw.len() {
1113                self.parse_error_at(
1114                    self.u32_from_usize(body_start + close),
1115                    0,
1116                    diagnostic_messages::UNTERMINATED_UNICODE_ESCAPE_SEQUENCE,
1117                    diagnostic_codes::UNTERMINATED_UNICODE_ESCAPE_SEQUENCE,
1118                );
1119                return None;
1120            }
1121            if raw[close] == b'}' {
1122                if !has_digit {
1123                    self.parse_error_at(
1124                        self.u32_from_usize(body_start + close),
1125                        1,
1126                        diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
1127                        diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
1128                    );
1129                }
1130            } else if !has_digit {
1131                self.parse_error_at(
1132                    self.u32_from_usize(body_start + j + 3),
1133                    1,
1134                    diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
1135                    diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
1136                );
1137            } else {
1138                self.parse_error_at(
1139                    self.u32_from_usize(body_start + close),
1140                    1,
1141                    diagnostic_messages::UNTERMINATED_UNICODE_ESCAPE_SEQUENCE,
1142                    diagnostic_codes::UNTERMINATED_UNICODE_ESCAPE_SEQUENCE,
1143                );
1144                self.parse_error_at(
1145                    self.u32_from_usize(body_start + close),
1146                    1,
1147                    diagnostic_messages::UNEXPECTED_DID_YOU_MEAN_TO_ESCAPE_IT_WITH_BACKSLASH,
1148                    diagnostic_codes::UNEXPECTED_DID_YOU_MEAN_TO_ESCAPE_IT_WITH_BACKSLASH,
1149                );
1150            }
1151            Some(close + 1)
1152        } else {
1153            let first = j + 2;
1154            let second = j + 3;
1155            let third = j + 4;
1156            let fourth = j + 5;
1157            if first >= raw.len() || !Self::is_hex_digit(raw[first]) {
1158                self.parse_error_at(
1159                    self.u32_from_usize(body_start + first),
1160                    u32::from(first < raw.len()),
1161                    diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
1162                    diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
1163                );
1164            } else if second >= raw.len() || !Self::is_hex_digit(raw[second]) {
1165                self.parse_error_at(
1166                    self.u32_from_usize(body_start + second),
1167                    u32::from(second < raw.len()),
1168                    diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
1169                    diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
1170                );
1171            } else if third >= raw.len() || !Self::is_hex_digit(raw[third]) {
1172                self.parse_error_at(
1173                    self.u32_from_usize(body_start + third),
1174                    u32::from(third < raw.len()),
1175                    diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
1176                    diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
1177                );
1178            } else if fourth >= raw.len() || !Self::is_hex_digit(raw[fourth]) {
1179                self.parse_error_at(
1180                    self.u32_from_usize(body_start + fourth),
1181                    u32::from(fourth < raw.len()),
1182                    diagnostic_messages::HEXADECIMAL_DIGIT_EXPECTED,
1183                    diagnostic_codes::HEXADECIMAL_DIGIT_EXPECTED,
1184                );
1185            }
1186            Some(j + 2)
1187        }
1188    }
1189
1190    // =========================================================================
1191    // Typed error helper methods (use these instead of parse_error_at_current_token)
1192    // =========================================================================
1193
1194    /// Error: Expression expected (TS1109)
1195    pub(crate) fn error_expression_expected(&mut self) {
1196        // Only emit error if we haven't already emitted one at this position
1197        // This prevents cascading TS1109 errors when TS1005 or other errors already reported
1198        // Use centralized error suppression heuristic
1199        if self.should_report_error() {
1200            use tsz_common::diagnostics::diagnostic_codes;
1201            self.parse_error_at_current_token(
1202                "Expression expected.",
1203                diagnostic_codes::EXPRESSION_EXPECTED,
1204            );
1205        }
1206    }
1207
1208    /// Error: Expression or comma expected (TS1137)
1209    /// Used in array literal element parsing where tsc uses TS1137 instead of TS1109.
1210    pub(crate) fn error_expression_or_comma_expected(&mut self) {
1211        if self.should_report_error() {
1212            use tsz_common::diagnostics::{diagnostic_codes, diagnostic_messages};
1213            self.parse_error_at_current_token(
1214                diagnostic_messages::EXPRESSION_OR_COMMA_EXPECTED,
1215                diagnostic_codes::EXPRESSION_OR_COMMA_EXPECTED,
1216            );
1217        }
1218    }
1219
1220    /// Error: Argument expression expected (TS1135)
1221    /// Used in function call argument list parsing instead of generic TS1109.
1222    pub(crate) fn error_argument_expression_expected(&mut self) {
1223        if self.should_report_error() {
1224            use tsz_common::diagnostics::diagnostic_codes;
1225            self.parse_error_at_current_token(
1226                "Argument expression expected.",
1227                diagnostic_codes::ARGUMENT_EXPRESSION_EXPECTED,
1228            );
1229        }
1230    }
1231
1232    /// Error: Type expected (TS1110)
1233    pub(crate) fn error_type_expected(&mut self) {
1234        use tsz_common::diagnostics::diagnostic_codes;
1235        self.parse_error_at_current_token("Type expected", diagnostic_codes::TYPE_EXPECTED);
1236    }
1237
1238    /// Error: Identifier expected (TS1003), or Invalid character (TS1127)
1239    pub(crate) fn error_identifier_expected(&mut self) {
1240        // When the current token is Unknown (invalid character), emit TS1127
1241        // instead of TS1003, matching tsc's behavior where the scanner's
1242        // TS1127 shadows the parser's TS1003 via position-based dedup.
1243        if self.is_token(SyntaxKind::Unknown) {
1244            if self.should_report_error() {
1245                use tsz_common::diagnostics::diagnostic_codes;
1246                self.parse_error_at_current_token(
1247                    "Invalid character.",
1248                    diagnostic_codes::INVALID_CHARACTER,
1249                );
1250            }
1251            return;
1252        }
1253        // Only emit error if we haven't already emitted one at this position
1254        // This prevents cascading errors when a missing token causes identifier to be expected
1255        // Use centralized error suppression heuristic
1256        if self.should_report_error() {
1257            use tsz_common::diagnostics::diagnostic_codes;
1258            self.parse_error_at_current_token(
1259                "Identifier expected",
1260                diagnostic_codes::IDENTIFIER_EXPECTED,
1261            );
1262        }
1263    }
1264
1265    /// Check if current token is a reserved word that cannot be used as an identifier
1266    /// Reserved words are keywords from `BreakKeyword` through `WithKeyword`
1267    #[inline]
1268    pub(crate) const fn is_reserved_word(&self) -> bool {
1269        // Match TypeScript's isReservedWord logic:
1270        // token >= SyntaxKind.FirstReservedWord && token <= SyntaxKind.LastReservedWord
1271        self.current_token as u16 >= SyntaxKind::FIRST_RESERVED_WORD as u16
1272            && self.current_token as u16 <= SyntaxKind::LAST_RESERVED_WORD as u16
1273    }
1274
1275    /// Get the text representation of the current keyword token
1276    const fn current_keyword_text(&self) -> &'static str {
1277        match self.current_token {
1278            SyntaxKind::BreakKeyword => "break",
1279            SyntaxKind::CaseKeyword => "case",
1280            SyntaxKind::CatchKeyword => "catch",
1281            SyntaxKind::ClassKeyword => "class",
1282            SyntaxKind::ConstKeyword => "const",
1283            SyntaxKind::ContinueKeyword => "continue",
1284            SyntaxKind::DebuggerKeyword => "debugger",
1285            SyntaxKind::DefaultKeyword => "default",
1286            SyntaxKind::DeleteKeyword => "delete",
1287            SyntaxKind::DoKeyword => "do",
1288            SyntaxKind::ElseKeyword => "else",
1289            SyntaxKind::EnumKeyword => "enum",
1290            SyntaxKind::ExportKeyword => "export",
1291            SyntaxKind::ExtendsKeyword => "extends",
1292            SyntaxKind::FalseKeyword => "false",
1293            SyntaxKind::FinallyKeyword => "finally",
1294            SyntaxKind::ForKeyword => "for",
1295            SyntaxKind::FunctionKeyword => "function",
1296            SyntaxKind::IfKeyword => "if",
1297            SyntaxKind::ImportKeyword => "import",
1298            SyntaxKind::InKeyword => "in",
1299            SyntaxKind::InstanceOfKeyword => "instanceof",
1300            SyntaxKind::NewKeyword => "new",
1301            SyntaxKind::NullKeyword => "null",
1302            SyntaxKind::ReturnKeyword => "return",
1303            SyntaxKind::SuperKeyword => "super",
1304            SyntaxKind::SwitchKeyword => "switch",
1305            SyntaxKind::ThisKeyword => "this",
1306            SyntaxKind::ThrowKeyword => "throw",
1307            SyntaxKind::TrueKeyword => "true",
1308            SyntaxKind::TryKeyword => "try",
1309            SyntaxKind::TypeOfKeyword => "typeof",
1310            SyntaxKind::VarKeyword => "var",
1311            SyntaxKind::VoidKeyword => "void",
1312            SyntaxKind::WhileKeyword => "while",
1313            SyntaxKind::WithKeyword => "with",
1314            _ => "reserved word",
1315        }
1316    }
1317
1318    /// Error: TS1359 - Identifier expected. '{0}' is a reserved word that cannot be used here.
1319    pub(crate) fn error_reserved_word_identifier(&mut self) {
1320        // Use centralized error suppression heuristic
1321        if self.should_report_error() {
1322            use tsz_common::diagnostics::diagnostic_codes;
1323            let word = self.current_keyword_text();
1324            if self.is_token(SyntaxKind::YieldKeyword) && self.in_generator_context() {
1325                self.parse_error_at_current_token(
1326                    "Identifier expected. 'yield' is a reserved word in strict mode.",
1327                    diagnostic_codes::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_IN_STRICT_MODE,
1328                );
1329                // Consume the reserved word token to prevent cascading errors
1330                self.next_token();
1331                return;
1332            }
1333            self.parse_error_at_current_token(
1334                &format!(
1335                    "Identifier expected. '{word}' is a reserved word that cannot be used here."
1336                ),
1337                diagnostic_codes::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_THAT_CANNOT_BE_USED_HERE,
1338            );
1339            // Consume the reserved word token to prevent cascading errors
1340            self.next_token();
1341        }
1342    }
1343
1344    /// Error: '{token}' expected (TS1005)
1345    pub(crate) fn error_token_expected(&mut self, token: &str) {
1346        // When the current token is Unknown (invalid character), emit only TS1127.
1347        // In tsc, the scanner emits TS1127 into parseDiagnostics via scanError callback
1348        // *before* the parser's parseExpected runs. Since tsc's parseErrorAtPosition dedup
1349        // suppresses errors at the same position as the last error, the parser's TS1005 is
1350        // always shadowed by the scanner's TS1127. We replicate this by emitting only TS1127.
1351        if self.is_token(SyntaxKind::Unknown) {
1352            if self.should_report_error() {
1353                use tsz_common::diagnostics::diagnostic_codes;
1354                self.parse_error_at_current_token(
1355                    "Invalid character.",
1356                    diagnostic_codes::INVALID_CHARACTER,
1357                );
1358            }
1359            return;
1360        }
1361        // Only emit error if we haven't already emitted one at this position
1362        // This prevents cascading errors when parse_semicolon() and similar functions call this
1363        // Use centralized error suppression heuristic
1364        if self.should_report_error() {
1365            use tsz_common::diagnostics::diagnostic_codes;
1366            self.parse_error_at_current_token(
1367                &format!("'{token}' expected."),
1368                diagnostic_codes::EXPECTED,
1369            );
1370        }
1371    }
1372
1373    /// Error: Comma expected (TS1005) - specifically for missing commas between parameters/arguments
1374    pub(crate) fn error_comma_expected(&mut self) {
1375        self.error_token_expected(",");
1376    }
1377
1378    /// Check if current token could start a parameter
1379    pub(crate) fn is_parameter_start(&mut self) -> bool {
1380        // Parameters can start with modifiers, identifiers, or binding patterns
1381        self.is_parameter_modifier()
1382            || self.is_token(SyntaxKind::AtToken) // decorators on parameters
1383            || self.is_token(SyntaxKind::DotDotDotToken) // rest parameter
1384            || self.is_identifier_or_keyword()
1385            || self.is_token(SyntaxKind::OpenBraceToken) // object binding pattern
1386            || self.is_token(SyntaxKind::OpenBracketToken) // array binding pattern
1387    }
1388
1389    /// Error: Unterminated template literal (TS1160)
1390    pub(crate) fn error_unterminated_template_literal_at(&mut self, start: u32, end: u32) {
1391        use tsz_common::diagnostics::diagnostic_codes;
1392        let length = end.saturating_sub(start).max(1);
1393        self.parse_error_at(
1394            start,
1395            length,
1396            "Unterminated template literal.",
1397            diagnostic_codes::UNTERMINATED_TEMPLATE_LITERAL,
1398        );
1399    }
1400
1401    /// Error: Declaration expected (TS1146)
1402    pub(crate) fn error_declaration_expected(&mut self) {
1403        use tsz_common::diagnostics::diagnostic_codes;
1404        self.parse_error_at_current_token(
1405            "Declaration expected",
1406            diagnostic_codes::DECLARATION_EXPECTED,
1407        );
1408    }
1409
1410    /// Error: Statement expected (TS1129)
1411    pub(crate) fn error_statement_expected(&mut self) {
1412        use tsz_common::diagnostics::diagnostic_codes;
1413        self.parse_error_at_current_token(
1414            "Statement expected",
1415            diagnostic_codes::STATEMENT_EXPECTED,
1416        );
1417    }
1418
1419    /// Check if a statement is a using/await using declaration not inside a block (TS1156)
1420    pub(crate) fn check_using_outside_block(&mut self, statement: NodeIndex) {
1421        use crate::parser::node_flags;
1422        use tsz_common::diagnostics::{diagnostic_codes, diagnostic_messages};
1423
1424        if statement.is_none() {
1425            return;
1426        }
1427
1428        // Get the node and check if it's a variable statement with using flags
1429        if let Some(node) = self.arena.get(statement) {
1430            // Check if it's a variable statement (not a block)
1431            if node.kind == syntax_kind_ext::VARIABLE_STATEMENT {
1432                // Check if it has using or await using flags
1433                let is_using = (node.flags
1434                    & self.u16_from_node_flags(node_flags::USING | node_flags::AWAIT_USING))
1435                    != 0;
1436                if is_using {
1437                    // Emit TS1156 error at the statement position
1438                    self.parse_error_at(
1439                        node.pos,
1440                        node.end.saturating_sub(node.pos).max(1),
1441                        diagnostic_messages::DECLARATIONS_CAN_ONLY_BE_DECLARED_INSIDE_A_BLOCK,
1442                        diagnostic_codes::DECLARATIONS_CAN_ONLY_BE_DECLARED_INSIDE_A_BLOCK,
1443                    );
1444                }
1445            }
1446        }
1447    }
1448
1449    /// Error: Unexpected token (TS1012)
1450    pub(crate) fn error_unexpected_token(&mut self) {
1451        use tsz_common::diagnostics::diagnostic_codes;
1452        self.parse_error_at_current_token("Unexpected token", diagnostic_codes::UNEXPECTED_TOKEN);
1453    }
1454
1455    /// Parse semicolon (or recover from missing)
1456    pub(crate) fn parse_semicolon(&mut self) {
1457        if self.is_token(SyntaxKind::SemicolonToken) {
1458            self.next_token();
1459        } else if self.is_token(SyntaxKind::Unknown) {
1460            // Scanner/lexer already reported an error for this token.
1461            // Avoid cascading TS1005 (';' expected) at the same position.
1462        } else if !self.can_parse_semicolon() {
1463            // Suppress cascading TS1005 "';' expected" when a recent error was already
1464            // emitted. This happens when a prior parse failure (e.g., missing identifier,
1465            // unsupported syntax) causes the parser to not consume tokens, then
1466            // parse_semicolon is called and fails too.
1467            // Use centralized error suppression heuristic
1468            if self.should_report_error() {
1469                self.error_token_expected(";");
1470            }
1471        }
1472    }
1473
1474    // =========================================================================
1475    // Keyword suggestion for misspelled keywords (TS1434/TS1435/TS1438)
1476    // =========================================================================
1477
1478    /// Provides a better error message than the generic "';' expected" for
1479    /// known common variants of a missing semicolon, such as misspelled keywords.
1480    ///
1481    /// Matches TypeScript's `parseErrorForMissingSemicolonAfter`.
1482    ///
1483    /// `expression` is the node index of the expression that was parsed before
1484    /// the missing semicolon.
1485    pub(crate) fn parse_error_for_missing_semicolon_after(&mut self, expression: NodeIndex) {
1486        use crate::parser::spelling;
1487        use tsz_common::diagnostics::diagnostic_codes;
1488
1489        let Some((pos, len, expression_text)) =
1490            self.missing_semicolon_after_expression_text(expression)
1491        else {
1492            if self.should_report_error() {
1493                self.error_token_expected(";");
1494            }
1495            return;
1496        };
1497
1498        if self.parse_missing_semicolon_keyword_error(pos, len, &expression_text) {
1499            return;
1500        }
1501
1502        if self.should_suppress_type_or_keyword_suggestion_for_missing_semicolon(
1503            expression_text.as_str(),
1504            pos,
1505        ) {
1506            return;
1507        }
1508
1509        if let Some(suggestion) = spelling::suggest_keyword(&expression_text) {
1510            if !self.should_suppress_type_or_keyword_suggestion_for_missing_semicolon(
1511                suggestion.as_str(),
1512                pos,
1513            ) {
1514                self.parse_error_at(
1515                    pos,
1516                    len,
1517                    &format!("Unknown keyword or identifier. Did you mean '{suggestion}'?"),
1518                    diagnostic_codes::UNKNOWN_KEYWORD_OR_IDENTIFIER_DID_YOU_MEAN,
1519                );
1520            }
1521            return;
1522        }
1523
1524        if self.is_token(SyntaxKind::Unknown) {
1525            return;
1526        }
1527
1528        if self.should_report_error() {
1529            self.parse_error_at(
1530                pos,
1531                len,
1532                "Unexpected keyword or identifier.",
1533                diagnostic_codes::UNEXPECTED_KEYWORD_OR_IDENTIFIER,
1534            );
1535        }
1536    }
1537
1538    fn missing_semicolon_after_expression_text(
1539        &self,
1540        expression: NodeIndex,
1541    ) -> Option<(u32, u32, String)> {
1542        let node = self.arena.get(expression)?;
1543
1544        if node.kind != SyntaxKind::Identifier as u16 {
1545            return None;
1546        }
1547
1548        // Use source text directly — arena identifier data may be empty for
1549        // identifiers created during parsing before data is fully populated.
1550        let source = self.scanner.source_text();
1551        let text = &source[node.pos as usize..node.end as usize];
1552        if text.is_empty() {
1553            return None;
1554        }
1555
1556        Some((node.pos, node.end - node.pos, text.to_string()))
1557    }
1558
1559    fn parse_missing_semicolon_keyword_error(
1560        &mut self,
1561        pos: u32,
1562        len: u32,
1563        expression_text: &str,
1564    ) -> bool {
1565        use tsz_common::diagnostics::diagnostic_codes;
1566
1567        match expression_text {
1568            "const" | "let" | "var" => {
1569                self.parse_error_at(
1570                    pos,
1571                    len,
1572                    "Variable declaration not allowed at this location.",
1573                    diagnostic_codes::VARIABLE_DECLARATION_NOT_ALLOWED_AT_THIS_LOCATION,
1574                );
1575                true
1576            }
1577            "declare" => true,
1578            "interface" => {
1579                if self.is_token(SyntaxKind::OpenBraceToken) {
1580                    self.parse_error_at_current_token(
1581                        "Interface must be given a name.",
1582                        diagnostic_codes::INTERFACE_MUST_BE_GIVEN_A_NAME,
1583                    );
1584                } else {
1585                    let name = self.scanner.get_token_value_ref().to_string();
1586                    self.parse_error_at_current_token(
1587                        &format!("Interface name cannot be '{name}'."),
1588                        diagnostic_codes::EXPECTED,
1589                    );
1590                }
1591                true
1592            }
1593            "type" => {
1594                self.parse_error_at_current_token("'=' expected.", diagnostic_codes::EXPECTED);
1595                true
1596            }
1597            _ => false,
1598        }
1599    }
1600
1601    fn should_suppress_type_or_keyword_suggestion_for_missing_semicolon(
1602        &self,
1603        text: &str,
1604        token_pos: u32,
1605    ) -> bool {
1606        if !matches!(
1607            text,
1608            "string"
1609                | "number"
1610                | "boolean"
1611                | "symbol"
1612                | "bigint"
1613                | "object"
1614                | "void"
1615                | "undefined"
1616                | "null"
1617                | "never"
1618                | "unknown"
1619                | "any"
1620        ) {
1621            return false;
1622        }
1623
1624        let source = self.scanner.source_text().as_bytes();
1625        let mut i = token_pos as usize;
1626        while i > 0 && source[i - 1].is_ascii_whitespace() {
1627            i -= 1;
1628        }
1629        i > 0 && source[i - 1] == b':'
1630    }
1631
1632    /// Check if we can parse a semicolon (ASI rules)
1633    /// Returns true if current token is semicolon or ASI applies
1634    ///
1635    /// ASI (Automatic Semicolon Insertion) rules (matching TypeScript):
1636    /// 1. Explicit semicolon
1637    /// 2. Before closing brace
1638    /// 3. At EOF
1639    /// 4. After line break (no additional checks!)
1640    ///
1641    /// Note: This matches TypeScript's `canParseSemicolon()` implementation exactly.
1642    /// The previous "enhanced" ASI with statement start checks was causing
1643    /// false-positive TS1005 errors because it was more restrictive than TypeScript.
1644    pub(crate) fn can_parse_semicolon(&self) -> bool {
1645        // Explicit semicolon
1646        if self.is_token(SyntaxKind::SemicolonToken) {
1647            return true;
1648        }
1649
1650        // ASI applies before closing brace
1651        if self.is_token(SyntaxKind::CloseBraceToken) {
1652            return true;
1653        }
1654
1655        // ASI applies at EOF
1656        if self.is_token(SyntaxKind::EndOfFileToken) {
1657            return true;
1658        }
1659
1660        // ASI applies after line break (matching TypeScript - no extra checks!)
1661        self.scanner.has_preceding_line_break()
1662    }
1663
1664    /// Check if ASI applies for restricted productions (return, throw, yield, break, continue)
1665    ///
1666    /// Restricted productions have special ASI rules:
1667    /// ASI applies immediately after a line break, WITHOUT checking if the next token starts a statement.
1668    ///
1669    /// Examples:
1670    /// - `return\nx` parses as `return; x;` (ASI applies due to line break)
1671    /// - `return x` parses as `return x;` (no ASI, x is the return value)
1672    /// - `throw\nx` parses as `throw; x;` (ASI applies due to line break)
1673    /// - `throw x` parses as `throw x;` (no ASI, x is the thrown value)
1674    pub(crate) fn can_parse_semicolon_for_restricted_production(&self) -> bool {
1675        // Explicit semicolon
1676        if self.is_token(SyntaxKind::SemicolonToken) {
1677            return true;
1678        }
1679
1680        // ASI applies before closing brace
1681        if self.is_token(SyntaxKind::CloseBraceToken) {
1682            return true;
1683        }
1684
1685        // ASI applies at EOF
1686        if self.is_token(SyntaxKind::EndOfFileToken) {
1687            return true;
1688        }
1689
1690        // ASI applies after line break (without checking statement start)
1691        // This is the key difference from can_parse_semicolon()
1692        if self.scanner.has_preceding_line_break() {
1693            return true;
1694        }
1695
1696        false
1697    }
1698
1699    // =========================================================================
1700    // Error Resynchronization
1701    // =========================================================================
1702
1703    /// Check if we're at a position where an expression can reasonably end
1704    /// This is used to suppress spurious TS1109 "expression expected" errors when
1705    /// the user has clearly moved on to the next statement/context.
1706    ///
1707    /// For TS1109 (expression expected), we should only suppress if we've reached a closing
1708    /// delimiter or EOF. We should NOT suppress on statement start keywords because if we're
1709    /// expecting an expression and see `var`, `let`, `function`, etc., that's likely an error.
1710    pub(crate) const fn is_at_expression_end(&self) -> bool {
1711        match self.token() {
1712            // Only tokens that naturally end expressions and indicate we've moved on
1713            SyntaxKind::SemicolonToken
1714            | SyntaxKind::CloseBraceToken
1715            | SyntaxKind::CloseParenToken
1716            | SyntaxKind::CloseBracketToken
1717            | SyntaxKind::EndOfFileToken => true,
1718            // NOTE: We do NOT suppress on statement start keywords
1719            // If we're expecting an expression and see `var`, `let`, `function`, etc.,
1720            // that's likely a genuine error where the user forgot the expression.
1721            // This fixes the "missing TS1109" issue where errors were being suppressed too aggressively.
1722            _ => false,
1723        }
1724    }
1725
1726    /// Check if current token can start a statement (synchronization point)
1727    pub(crate) const fn is_statement_start(&self) -> bool {
1728        matches!(
1729            self.token(),
1730            SyntaxKind::VarKeyword
1731                | SyntaxKind::LetKeyword
1732                | SyntaxKind::ConstKeyword
1733                | SyntaxKind::FunctionKeyword
1734                | SyntaxKind::ClassKeyword
1735                | SyntaxKind::IfKeyword
1736                | SyntaxKind::ForKeyword
1737                | SyntaxKind::WhileKeyword
1738                | SyntaxKind::DoKeyword
1739                | SyntaxKind::SwitchKeyword
1740                | SyntaxKind::TryKeyword
1741                | SyntaxKind::CatchKeyword
1742                | SyntaxKind::FinallyKeyword
1743                | SyntaxKind::WithKeyword
1744                | SyntaxKind::DebuggerKeyword
1745                | SyntaxKind::ReturnKeyword
1746                | SyntaxKind::BreakKeyword
1747                | SyntaxKind::ContinueKeyword
1748                | SyntaxKind::ThrowKeyword
1749                | SyntaxKind::YieldKeyword
1750                | SyntaxKind::AsyncKeyword
1751                | SyntaxKind::UsingKeyword
1752                | SyntaxKind::AwaitKeyword
1753                | SyntaxKind::InterfaceKeyword
1754                | SyntaxKind::TypeKeyword
1755                | SyntaxKind::EnumKeyword
1756                | SyntaxKind::NamespaceKeyword
1757                | SyntaxKind::ModuleKeyword
1758                | SyntaxKind::ImportKeyword
1759                | SyntaxKind::ExportKeyword
1760                | SyntaxKind::DeclareKeyword
1761                | SyntaxKind::Identifier
1762                | SyntaxKind::StringLiteral
1763                | SyntaxKind::AtToken
1764                | SyntaxKind::NumericLiteral
1765                | SyntaxKind::BigIntLiteral
1766                | SyntaxKind::TrueKeyword
1767                | SyntaxKind::FalseKeyword
1768                | SyntaxKind::NullKeyword
1769                | SyntaxKind::ThisKeyword
1770                | SyntaxKind::SuperKeyword
1771                | SyntaxKind::ExclamationToken
1772                | SyntaxKind::TildeToken
1773                | SyntaxKind::PlusToken
1774                | SyntaxKind::MinusToken
1775                | SyntaxKind::PlusPlusToken
1776                | SyntaxKind::MinusMinusToken
1777                | SyntaxKind::TypeOfKeyword
1778                | SyntaxKind::VoidKeyword
1779                | SyntaxKind::DeleteKeyword
1780                | SyntaxKind::OpenBraceToken
1781                | SyntaxKind::SemicolonToken
1782                | SyntaxKind::OpenParenToken
1783                | SyntaxKind::OpenBracketToken
1784                | SyntaxKind::LessThanToken
1785        )
1786    }
1787
1788    /// Check if current token is a synchronization point for error recovery
1789    /// This includes statement starts plus additional keywords that indicate
1790    /// boundaries in control structures (else, case, default, catch, finally, etc.)
1791    pub(crate) const fn is_resync_sync_point(&self) -> bool {
1792        self.is_statement_start()
1793            || matches!(
1794                self.token(),
1795                SyntaxKind::ElseKeyword
1796                    | SyntaxKind::CaseKeyword
1797                    | SyntaxKind::DefaultKeyword
1798                    | SyntaxKind::CatchKeyword
1799                    | SyntaxKind::FinallyKeyword
1800                    | SyntaxKind::CommaToken
1801            )
1802    }
1803
1804    /// Resynchronize after a parse error by skipping to the next statement boundary
1805    /// This prevents cascading errors by finding a known good synchronization point.
1806    /// `allow_statement_starts` controls whether token kinds that begin statements
1807    /// (especially identifiers) are valid sync points.
1808    pub(crate) fn resync_after_error_with_statement_starts(
1809        &mut self,
1810        allow_statement_starts: bool,
1811    ) {
1812        // If we're already at a sync point or EOF, no need to resync
1813        if self.is_resync_sync_point_with_statement_starts(allow_statement_starts) {
1814            return;
1815        }
1816
1817        // Skip tokens until we find a synchronization point
1818        let mut brace_depth = 0u32;
1819        let mut paren_depth = 0u32;
1820        let mut bracket_depth = 0u32;
1821        let max_iterations = 1000; // Prevent infinite loops
1822
1823        for _ in 0..max_iterations {
1824            // Check for EOF
1825            if self.is_token(SyntaxKind::EndOfFileToken) {
1826                break;
1827            }
1828
1829            // Track nesting depth to handle nested structures
1830            match self.token() {
1831                SyntaxKind::OpenBraceToken => {
1832                    brace_depth += 1;
1833                    self.next_token();
1834                    continue;
1835                }
1836                SyntaxKind::CloseBraceToken => {
1837                    if brace_depth > 0 {
1838                        brace_depth -= 1;
1839                        self.next_token();
1840                        continue;
1841                    }
1842                    // Found closing brace at same level - this is a sync point
1843                    self.next_token();
1844                    break;
1845                }
1846                SyntaxKind::OpenParenToken => {
1847                    paren_depth += 1;
1848                    self.next_token();
1849                    continue;
1850                }
1851                SyntaxKind::CloseParenToken => {
1852                    if paren_depth > 0 {
1853                        paren_depth -= 1;
1854                        self.next_token();
1855                        continue;
1856                    }
1857                    // Found closing paren at same level - could be end of expression
1858                    // Skip it and check if next token is a sync point
1859                    self.next_token();
1860                    if self.is_resync_sync_point_with_statement_starts(allow_statement_starts) {
1861                        break;
1862                    }
1863                    continue;
1864                }
1865                SyntaxKind::OpenBracketToken => {
1866                    bracket_depth += 1;
1867                    self.next_token();
1868                    continue;
1869                }
1870                SyntaxKind::CloseBracketToken => {
1871                    if bracket_depth > 0 {
1872                        bracket_depth -= 1;
1873                        self.next_token();
1874                        continue;
1875                    }
1876                    // Found closing bracket at same level - skip it
1877                    self.next_token();
1878                    continue;
1879                }
1880                SyntaxKind::SemicolonToken => {
1881                    // Semicolon is always a sync point (even in nested contexts)
1882                    self.next_token();
1883                    break;
1884                }
1885                _ => {}
1886            }
1887
1888            // If we're at depth 0 and found a sync point, we've resync'd
1889            if brace_depth == 0
1890                && paren_depth == 0
1891                && bracket_depth == 0
1892                && self.is_resync_sync_point_with_statement_starts(allow_statement_starts)
1893            {
1894                break;
1895            }
1896
1897            // Keep skipping tokens
1898            self.next_token();
1899        }
1900    }
1901
1902    fn is_resync_sync_point_with_statement_starts(&self, allow_statement_starts: bool) -> bool {
1903        self.is_resync_sync_point()
1904            && (allow_statement_starts || self.token() != SyntaxKind::Identifier)
1905    }
1906
1907    /// Default resync behavior: allow statement starts as sync points.
1908    pub(crate) fn resync_after_error(&mut self) {
1909        self.resync_after_error_with_statement_starts(true);
1910    }
1911
1912    // =========================================================================
1913    // Expression-Level Error Recovery
1914    // =========================================================================
1915
1916    /// Check if current token can start an expression
1917    pub(crate) const fn is_expression_start(&self) -> bool {
1918        matches!(
1919            self.token(),
1920            SyntaxKind::NumericLiteral
1921                | SyntaxKind::BigIntLiteral
1922                | SyntaxKind::StringLiteral
1923                | SyntaxKind::NoSubstitutionTemplateLiteral
1924                | SyntaxKind::TemplateHead
1925                | SyntaxKind::TemplateMiddle
1926                | SyntaxKind::TemplateTail
1927                | SyntaxKind::TrueKeyword
1928                | SyntaxKind::FalseKeyword
1929                | SyntaxKind::NullKeyword
1930                | SyntaxKind::Identifier
1931                | SyntaxKind::ThisKeyword
1932                | SyntaxKind::SuperKeyword
1933                | SyntaxKind::ImportKeyword
1934                | SyntaxKind::TypeKeyword
1935                | SyntaxKind::AnyKeyword
1936                | SyntaxKind::StringKeyword
1937                | SyntaxKind::NumberKeyword
1938                | SyntaxKind::BooleanKeyword
1939                | SyntaxKind::SymbolKeyword
1940                | SyntaxKind::BigIntKeyword
1941                | SyntaxKind::ObjectKeyword
1942                | SyntaxKind::NeverKeyword
1943                | SyntaxKind::UnknownKeyword
1944                | SyntaxKind::UndefinedKeyword
1945                | SyntaxKind::RequireKeyword
1946                | SyntaxKind::ModuleKeyword
1947                | SyntaxKind::NamespaceKeyword
1948                | SyntaxKind::AsyncKeyword
1949                | SyntaxKind::AwaitKeyword
1950                | SyntaxKind::YieldKeyword
1951                | SyntaxKind::LetKeyword
1952                | SyntaxKind::NewKeyword
1953                | SyntaxKind::ClassKeyword
1954                | SyntaxKind::FunctionKeyword
1955                | SyntaxKind::DeleteKeyword
1956                | SyntaxKind::VoidKeyword
1957                | SyntaxKind::TypeOfKeyword
1958                | SyntaxKind::InstanceOfKeyword
1959                | SyntaxKind::StaticKeyword
1960                | SyntaxKind::AbstractKeyword
1961                | SyntaxKind::OverrideKeyword
1962                | SyntaxKind::ReadonlyKeyword
1963                | SyntaxKind::AccessorKeyword
1964                | SyntaxKind::GetKeyword
1965                | SyntaxKind::SetKeyword
1966                | SyntaxKind::DeclareKeyword
1967                | SyntaxKind::PublicKeyword
1968                | SyntaxKind::ProtectedKeyword
1969                | SyntaxKind::PrivateKeyword
1970                | SyntaxKind::OfKeyword
1971                | SyntaxKind::SatisfiesKeyword
1972                | SyntaxKind::FromKeyword
1973                | SyntaxKind::AsKeyword
1974                | SyntaxKind::IsKeyword
1975                | SyntaxKind::AssertKeyword
1976                | SyntaxKind::AssertsKeyword
1977                | SyntaxKind::IntrinsicKeyword
1978                | SyntaxKind::OutKeyword
1979                | SyntaxKind::InferKeyword
1980                | SyntaxKind::ConstructorKeyword
1981                | SyntaxKind::UsingKeyword
1982                | SyntaxKind::KeyOfKeyword
1983                | SyntaxKind::UniqueKeyword
1984                | SyntaxKind::GlobalKeyword
1985                | SyntaxKind::InterfaceKeyword
1986                | SyntaxKind::EnumKeyword
1987                | SyntaxKind::DeferKeyword
1988                | SyntaxKind::PrivateIdentifier
1989                | SyntaxKind::PlusToken
1990                | SyntaxKind::MinusToken
1991                | SyntaxKind::AsteriskToken
1992                | SyntaxKind::TildeToken
1993                | SyntaxKind::ExclamationToken
1994                | SyntaxKind::PlusPlusToken
1995                | SyntaxKind::MinusMinusToken
1996                | SyntaxKind::OpenParenToken
1997                | SyntaxKind::OpenBracketToken
1998                | SyntaxKind::OpenBraceToken
1999                | SyntaxKind::LessThanToken
2000                | SyntaxKind::SlashToken
2001                | SyntaxKind::SlashEqualsToken
2002                | SyntaxKind::AtToken
2003        )
2004    }
2005
2006    /// Check if current token is a binary operator
2007    pub(crate) const fn is_binary_operator(&self) -> bool {
2008        let precedence = self.get_operator_precedence(self.token());
2009        precedence > 0
2010    }
2011
2012    /// Resynchronize to next expression boundary after parse error
2013    pub(crate) fn resync_to_next_expression_boundary(&mut self) {
2014        let max_iterations = 100;
2015        for _ in 0..max_iterations {
2016            if self.is_token(SyntaxKind::EndOfFileToken) {
2017                break;
2018            }
2019            if self.is_expression_boundary() {
2020                break;
2021            }
2022            if self.is_binary_operator() {
2023                break;
2024            }
2025            if self.is_expression_start() {
2026                break;
2027            }
2028            self.next_token();
2029        }
2030    }
2031
2032    /// Check if current token is at an expression boundary (a natural stopping point)
2033    pub(crate) const fn is_expression_boundary(&self) -> bool {
2034        matches!(
2035            self.token(),
2036            SyntaxKind::SemicolonToken
2037                | SyntaxKind::CloseBraceToken
2038                | SyntaxKind::CloseParenToken
2039                | SyntaxKind::CloseBracketToken
2040                | SyntaxKind::CommaToken
2041                | SyntaxKind::ColonToken
2042                | SyntaxKind::CaseKeyword
2043                | SyntaxKind::DefaultKeyword
2044                | SyntaxKind::ElseKeyword
2045                | SyntaxKind::WhileKeyword // for do-while
2046                | SyntaxKind::AsKeyword
2047                | SyntaxKind::SatisfiesKeyword
2048        )
2049    }
2050
2051    /// Create a missing expression placeholder for error recovery.
2052    /// This allows the AST to remain structurally valid even when an expression is missing.
2053    pub(crate) fn create_missing_expression(&mut self) -> NodeIndex {
2054        let pos = self.token_pos();
2055        // Create an identifier with empty text to represent missing expression
2056        self.arena.add_identifier(
2057            SyntaxKind::Identifier as u16,
2058            pos,
2059            pos,
2060            IdentifierData {
2061                atom: Atom::NONE,
2062                escaped_text: String::new(),
2063                original_text: None,
2064                type_arguments: None,
2065            },
2066        )
2067    }
2068
2069    /// Try to recover from a missing right-hand operand in a binary expression.
2070    /// Returns a placeholder expression if recovery is possible.
2071    pub(crate) fn try_recover_binary_rhs(&mut self) -> NodeIndex {
2072        // If we're at an expression boundary after an operator, create a placeholder
2073        if self.is_expression_boundary() || self.is_statement_start() {
2074            self.create_missing_expression()
2075        } else {
2076            NodeIndex::NONE
2077        }
2078    }
2079
2080    /// Try to rescan `>` as a compound token (`>>`, `>>>`, `>=`, `>>=`, `>>>=`)
2081    /// Returns the rescanned token (which may be unchanged if no compound token found)
2082    pub(crate) fn try_rescan_greater_token(&mut self) -> SyntaxKind {
2083        if self.current_token == SyntaxKind::GreaterThanToken {
2084            self.current_token = self.scanner.re_scan_greater_token();
2085        }
2086        self.current_token
2087    }
2088
2089    /// Parse expected `>` token, handling compound tokens like `>>` and `>>>`
2090    /// When we have `>>`, we need to consume just one `>` and leave `>` for the next parse
2091    pub(crate) fn parse_expected_greater_than(&mut self) {
2092        match self.current_token {
2093            SyntaxKind::GreaterThanToken => {
2094                // Simple case - just consume the single `>`
2095                self.next_token();
2096            }
2097            SyntaxKind::GreaterThanGreaterThanToken => {
2098                // `>>` - back up scanner and treat as single `>`
2099                // After consuming, the remaining `>` becomes the current token
2100                self.scanner.set_pos(self.scanner.get_pos() - 1);
2101                self.current_token = SyntaxKind::GreaterThanToken;
2102            }
2103            SyntaxKind::GreaterThanGreaterThanGreaterThanToken => {
2104                // `>>>` - back up scanner and treat as single `>`
2105                // After consuming, the remaining `>>` becomes the current token
2106                self.scanner.set_pos(self.scanner.get_pos() - 2);
2107                self.current_token = SyntaxKind::GreaterThanGreaterThanToken;
2108            }
2109            SyntaxKind::GreaterThanEqualsToken => {
2110                // `>=` - back up scanner and treat as single `>`
2111                self.scanner.set_pos(self.scanner.get_pos() - 1);
2112                self.current_token = SyntaxKind::EqualsToken;
2113            }
2114            SyntaxKind::GreaterThanGreaterThanEqualsToken => {
2115                // `>>=` - back up scanner and treat as single `>`
2116                self.scanner.set_pos(self.scanner.get_pos() - 2);
2117                self.current_token = SyntaxKind::GreaterThanEqualsToken;
2118            }
2119            SyntaxKind::GreaterThanGreaterThanGreaterThanEqualsToken => {
2120                // `>>>=` - back up scanner and treat as single `>`
2121                self.scanner.set_pos(self.scanner.get_pos() - 3);
2122                self.current_token = SyntaxKind::GreaterThanGreaterThanEqualsToken;
2123            }
2124            _ => {
2125                // error_token_expected already has error suppression check
2126                self.error_token_expected(">");
2127            }
2128        }
2129    }
2130
2131    /// Check if the current token starts with `<` (includes `<<` and `<<=`).
2132    /// Mirrors `is_greater_than_or_compound` for the opening side.
2133    pub(crate) const fn is_less_than_or_compound(&self) -> bool {
2134        matches!(
2135            self.current_token,
2136            SyntaxKind::LessThanToken
2137                | SyntaxKind::LessThanLessThanToken
2138                | SyntaxKind::LessThanLessThanEqualsToken
2139        )
2140    }
2141
2142    /// Consume a single `<` from the current token.
2143    /// Handles compound tokens like `<<` and `<<=` by leaving the scanner
2144    /// position unchanged (past the compound token) and setting `current_token`
2145    /// to the remainder. Unlike `>`, `<` is eagerly combined by the scanner,
2146    /// so we cannot back up—the scanner would re-combine. Instead, we leave
2147    /// pos past the compound and set `current_token` to the remainder.
2148    /// When the remainder is later consumed via `parse_expected(<)` →
2149    /// `next_token()`, the scanner scans from past the compound, correctly
2150    /// yielding the token that follows.
2151    pub(crate) fn parse_expected_less_than(&mut self) {
2152        match self.current_token {
2153            SyntaxKind::LessThanToken => {
2154                self.next_token();
2155            }
2156            SyntaxKind::LessThanLessThanToken => {
2157                // `<<` → consume first `<`, remainder is `<`
2158                // Scanner pos stays past `<<`; when the second `<` is consumed,
2159                // next_token() will scan from past both, yielding the following token.
2160                self.current_token = SyntaxKind::LessThanToken;
2161            }
2162            SyntaxKind::LessThanLessThanEqualsToken => {
2163                // `<<=` → consume first `<`, remainder is `<=`
2164                // Scanner pos stays past `<<=`; same logic as above.
2165                self.current_token = SyntaxKind::LessThanEqualsToken;
2166            }
2167            _ => {
2168                self.error_token_expected("<");
2169            }
2170        }
2171    }
2172
2173    /// Create a `NodeList` from a Vec of `NodeIndex`
2174    pub(crate) const fn make_node_list(&self, nodes: Vec<NodeIndex>) -> NodeList {
2175        let _ = self;
2176        NodeList {
2177            nodes,
2178            pos: 0,
2179            end: 0,
2180            has_trailing_comma: false,
2181        }
2182    }
2183
2184    /// Get operator precedence
2185    pub(crate) const fn get_operator_precedence(&self, token: SyntaxKind) -> u8 {
2186        match token {
2187            SyntaxKind::CommaToken => 1,
2188            SyntaxKind::EqualsToken
2189            | SyntaxKind::PlusEqualsToken
2190            | SyntaxKind::MinusEqualsToken
2191            | SyntaxKind::AsteriskEqualsToken
2192            | SyntaxKind::AsteriskAsteriskEqualsToken
2193            | SyntaxKind::SlashEqualsToken
2194            | SyntaxKind::PercentEqualsToken
2195            | SyntaxKind::LessThanLessThanEqualsToken
2196            | SyntaxKind::GreaterThanGreaterThanEqualsToken
2197            | SyntaxKind::GreaterThanGreaterThanGreaterThanEqualsToken
2198            | SyntaxKind::AmpersandEqualsToken
2199            | SyntaxKind::BarEqualsToken
2200            | SyntaxKind::BarBarEqualsToken
2201            | SyntaxKind::AmpersandAmpersandEqualsToken
2202            | SyntaxKind::QuestionQuestionEqualsToken
2203            | SyntaxKind::CaretEqualsToken => 2,
2204            SyntaxKind::QuestionToken => 3,
2205            SyntaxKind::BarBarToken | SyntaxKind::QuestionQuestionToken => 4,
2206            SyntaxKind::AmpersandAmpersandToken => 5,
2207            SyntaxKind::BarToken => 6,
2208            SyntaxKind::CaretToken => 7,
2209            SyntaxKind::AmpersandToken => 8,
2210            SyntaxKind::EqualsEqualsToken
2211            | SyntaxKind::ExclamationEqualsToken
2212            | SyntaxKind::EqualsEqualsEqualsToken
2213            | SyntaxKind::ExclamationEqualsEqualsToken => 9,
2214            // 'in' is not a binary operator in for-statement initializers
2215            SyntaxKind::InKeyword => {
2216                if self.in_disallow_in_context() {
2217                    0
2218                } else {
2219                    10
2220                }
2221            }
2222            SyntaxKind::LessThanToken
2223            | SyntaxKind::GreaterThanToken
2224            | SyntaxKind::LessThanEqualsToken
2225            | SyntaxKind::GreaterThanEqualsToken
2226            | SyntaxKind::InstanceOfKeyword
2227            | SyntaxKind::AsKeyword
2228            | SyntaxKind::SatisfiesKeyword => 10,
2229            SyntaxKind::LessThanLessThanToken
2230            | SyntaxKind::GreaterThanGreaterThanToken
2231            | SyntaxKind::GreaterThanGreaterThanGreaterThanToken => 11,
2232            SyntaxKind::PlusToken | SyntaxKind::MinusToken => 12,
2233            SyntaxKind::AsteriskToken | SyntaxKind::SlashToken | SyntaxKind::PercentToken => 13,
2234            SyntaxKind::AsteriskAsteriskToken => 14,
2235            _ => 0,
2236        }
2237    }
2238
2239    /// Push a new label scope (called when entering a function or module)
2240    pub(crate) fn push_label_scope(&mut self) {
2241        let new_depth = self.label_scopes.len() + 1;
2242        trace!(pos = self.token_pos(), new_depth, "push_label_scope");
2243        self.label_scopes.push(FxHashMap::default());
2244    }
2245
2246    /// Pop the current label scope (called when exiting a function or module)
2247    pub(crate) fn pop_label_scope(&mut self) {
2248        let old_depth = self.label_scopes.len();
2249        trace!(pos = self.token_pos(), old_depth, "pop_label_scope");
2250        self.label_scopes.pop();
2251    }
2252
2253    /// Check if a label already exists in the current scope, and if so, emit TS1114.
2254    /// Returns true if the label is a duplicate.
2255    pub(crate) fn check_duplicate_label(&mut self, label_name: &str, label_pos: u32) -> bool {
2256        let scope_depth = self.label_scopes.len();
2257        trace!(label_name, label_pos, scope_depth, "check_duplicate_label");
2258        if let Some(current_scope) = self.label_scopes.last_mut() {
2259            if current_scope.contains_key(label_name) {
2260                // Duplicate label - emit TS1114
2261                use tsz_common::diagnostics::diagnostic_codes;
2262                let message = format!("Duplicate label '{label_name}'.");
2263                trace!(label_name, "duplicate label found");
2264                self.parse_error_at(
2265                    label_pos,
2266                    self.u32_from_usize(label_name.len()),
2267                    &message,
2268                    diagnostic_codes::DUPLICATE_LABEL,
2269                );
2270                return true;
2271            }
2272            // Not a duplicate - record this label
2273            trace!(label_name, "adding label to scope");
2274            current_scope.insert(label_name.to_string(), label_pos);
2275        }
2276        false
2277    }
2278}
2279
2280// Integration tests for parse_error_for_missing_semicolon_after live in
2281// parser/tests/spelling_integration_tests.rs.  Pure spelling-logic tests
2282// live in parser/spelling.rs.