Skip to main content

tsz_checker/declarations/
declarations.rs

1//! Declaration Type Checking
2//!
3//! Handles classes, interfaces, functions, and variable declarations.
4//! This module separates declaration checking logic from the monolithic `CheckerState`.
5
6use crate::context::CheckerContext;
7use tsz_parser::parser::{NodeIndex, node_flags, syntax_kind_ext};
8use tsz_scanner::SyntaxKind;
9
10/// Declaration type checker that operates on the shared context.
11///
12/// This is a stateless checker that borrows the context mutably.
13/// All declaration type checking goes through this checker.
14pub struct DeclarationChecker<'a, 'ctx> {
15    pub ctx: &'a mut CheckerContext<'ctx>,
16}
17
18impl<'a, 'ctx> DeclarationChecker<'a, 'ctx> {
19    /// Create a new declaration checker with a mutable context reference.
20    pub const fn new(ctx: &'a mut CheckerContext<'ctx>) -> Self {
21        Self { ctx }
22    }
23
24    /// Check if a declaration is ambient (has declare keyword, AMBIENT node flag,
25    /// or is inside an ambient context like `declare module`).
26    pub(crate) fn is_ambient_declaration(&self, var_idx: NodeIndex) -> bool {
27        // .d.ts files are always ambient
28        if self.ctx.file_name.ends_with(".d.ts") {
29            return true;
30        }
31
32        // Check if the node itself has a `declare` modifier
33        if let Some(node) = self.ctx.arena.get(var_idx)
34            && self.node_has_declare_modifier(var_idx, node)
35        {
36            return true;
37        }
38
39        // Check if the node or any ancestor has the AMBIENT flag or `declare` modifier
40        let mut current = var_idx;
41        while current.is_some() {
42            if let Some(node) = self.ctx.arena.get(current) {
43                if (node.flags as u32) & node_flags::AMBIENT != 0 {
44                    return true;
45                }
46                // Check if this ancestor has the `declare` keyword modifier
47                // (covers `declare module`, `declare namespace`, `declare class`, etc.)
48                if self.node_has_declare_modifier_any(current, node) {
49                    return true;
50                }
51
52                // Move to parent
53                if let Some(ext) = self.ctx.arena.get_extended(current) {
54                    current = ext.parent;
55                } else {
56                    break;
57                }
58            } else {
59                break;
60            }
61        }
62        false
63    }
64
65    /// Check if a node (class or function declaration) has the `declare` keyword modifier.
66    fn node_has_declare_modifier(
67        &self,
68        _node_idx: NodeIndex,
69        node: &tsz_parser::parser::node::Node,
70    ) -> bool {
71        let modifiers = if let Some(class) = self.ctx.arena.get_class(node) {
72            &class.modifiers
73        } else if let Some(func) = self.ctx.arena.get_function(node) {
74            &func.modifiers
75        } else {
76            return false;
77        };
78        self.modifiers_contain_declare(modifiers)
79    }
80
81    /// Check if any node type (class, function, module, variable, enum, etc.) has `declare`.
82    fn node_has_declare_modifier_any(
83        &self,
84        _node_idx: NodeIndex,
85        node: &tsz_parser::parser::node::Node,
86    ) -> bool {
87        // Try each node type that can carry modifiers
88        let modifiers = if let Some(class) = self.ctx.arena.get_class(node) {
89            &class.modifiers
90        } else if let Some(func) = self.ctx.arena.get_function(node) {
91            &func.modifiers
92        } else if node.kind == syntax_kind_ext::MODULE_DECLARATION {
93            // Module declarations store modifiers differently
94            if let Some(module) = self.ctx.arena.get_module(node) {
95                &module.modifiers
96            } else {
97                return false;
98            }
99        } else if let Some(var_data) = self.ctx.arena.get_variable(node) {
100            &var_data.modifiers
101        } else if let Some(enum_decl) = self.ctx.arena.get_enum(node) {
102            &enum_decl.modifiers
103        } else {
104            return false;
105        };
106        self.modifiers_contain_declare(modifiers)
107    }
108
109    /// Check if a modifier list contains the `declare` keyword.
110    fn modifiers_contain_declare(&self, modifiers: &Option<tsz_parser::parser::NodeList>) -> bool {
111        if let Some(mods) = modifiers {
112            for &mod_idx in &mods.nodes {
113                if let Some(mod_node) = self.ctx.arena.get(mod_idx)
114                    && mod_node.kind == SyntaxKind::DeclareKeyword as u16
115                {
116                    return true;
117                }
118            }
119        }
120        false
121    }
122
123    /// Check a declaration node.
124    ///
125    /// This dispatches to specialized handlers based on declaration kind.
126    /// Currently a skeleton - logic will be migrated incrementally from `CheckerState`.
127    pub fn check(&mut self, decl_idx: NodeIndex) {
128        let Some(node) = self.ctx.arena.get(decl_idx) else {
129            return;
130        };
131
132        match node.kind {
133            k if k == syntax_kind_ext::VARIABLE_STATEMENT => {
134                self.check_variable_statement(decl_idx);
135            }
136            k if k == syntax_kind_ext::FUNCTION_DECLARATION => {
137                self.check_function_declaration(decl_idx);
138            }
139            k if k == syntax_kind_ext::CLASS_DECLARATION => {
140                self.check_class_declaration(decl_idx);
141            }
142            k if k == syntax_kind_ext::INTERFACE_DECLARATION => {
143                self.check_interface_declaration(decl_idx);
144            }
145            k if k == syntax_kind_ext::TYPE_ALIAS_DECLARATION => {
146                self.check_type_alias_declaration(decl_idx);
147            }
148            k if k == syntax_kind_ext::ENUM_DECLARATION => {
149                self.check_enum_declaration(decl_idx);
150            }
151            k if k == syntax_kind_ext::MODULE_DECLARATION => {
152                self.check_module_declaration(decl_idx);
153            }
154            _ => {
155                // Unhandled declaration types - will be expanded incrementally
156            }
157        }
158    }
159
160    /// Check a variable statement.
161    pub fn check_variable_statement(&mut self, stmt_idx: NodeIndex) {
162        let Some(node) = self.ctx.arena.get(stmt_idx) else {
163            return;
164        };
165
166        if let Some(var_stmt) = self.ctx.arena.get_variable(node) {
167            for &decl_idx in &var_stmt.declarations.nodes {
168                self.check_variable_declaration(decl_idx);
169            }
170        }
171    }
172
173    /// Check a variable declaration list.
174    pub fn check_variable_declaration_list(&mut self, list_idx: NodeIndex) {
175        let Some(node) = self.ctx.arena.get(list_idx) else {
176            return;
177        };
178
179        if let Some(var_list) = self.ctx.arena.get_variable(node) {
180            for &decl_idx in &var_list.declarations.nodes {
181                self.check_variable_declaration(decl_idx);
182            }
183        }
184    }
185
186    /// Check a variable declaration.
187    pub fn check_variable_declaration(&mut self, decl_idx: NodeIndex) {
188        let Some(decl_node) = self.ctx.arena.get(decl_idx) else {
189            return;
190        };
191        let Some(decl_data) = self.ctx.arena.get_variable_declaration(decl_node) else {
192            return;
193        };
194
195        // TS1155: Check if const declarations must be initialized
196        // Get the parent node (VARIABLE_DECLARATION_LIST) via extended info
197        let parent_idx = if let Some(ext) = self.ctx.arena.get_extended(decl_idx) {
198            ext.parent
199        } else {
200            return;
201        };
202
203        let Some(parent_node) = self.ctx.arena.get(parent_idx) else {
204            return;
205        };
206
207        // Check if this is a const declaration by checking the parent's flags
208        let is_const = (parent_node.flags & node_flags::CONST as u16) != 0;
209
210        // TS1155: 'const' declarations must be initialized
211        if is_const && decl_data.initializer.is_none() {
212            // Skip for destructuring patterns - they get TS1182 from the parser
213            if let Some(name_node) = self.ctx.arena.get(decl_data.name) {
214                if name_node.kind == syntax_kind_ext::OBJECT_BINDING_PATTERN
215                    || name_node.kind == syntax_kind_ext::ARRAY_BINDING_PATTERN
216                {
217                    // TS1182 is emitted by parser for destructuring without initializer
218                    // Don't also emit TS1155
219                } else {
220                    // Check if this is in a for-in or for-of loop (allowed)
221                    if let Some(parent_ext) = self.ctx.arena.get_extended(parent_idx)
222                        && let Some(gp_node) = self.ctx.arena.get(parent_ext.parent)
223                        && (gp_node.kind == syntax_kind_ext::FOR_IN_STATEMENT
224                            || gp_node.kind == syntax_kind_ext::FOR_OF_STATEMENT)
225                    {
226                        // const in for-in/for-of is allowed without initializer
227                        return;
228                    }
229
230                    // Check if this is an ambient declaration (allowed)
231                    let is_ambient = self.is_ambient_declaration(decl_idx);
232                    if is_ambient {
233                        return;
234                    }
235
236                    self.ctx.error(
237                        decl_node.pos,
238                        decl_node.end - decl_node.pos,
239                        "'const' declarations must be initialized.".to_string(),
240                        1155,
241                    );
242                }
243            }
244        }
245
246        // Variable declaration checking is handled by CheckerState for now
247        // Will be migrated incrementally
248        // Key checks:
249        // - Type annotation vs initializer type compatibility
250        // - Adding variable to scope
251    }
252
253    /// Check a function declaration.
254    pub fn check_function_declaration(&mut self, func_idx: NodeIndex) {
255        let Some(node) = self.ctx.arena.get(func_idx) else {
256            return;
257        };
258        let Some(func) = self.ctx.arena.get_function(node) else {
259            return;
260        };
261
262        // TS2371: Check for parameter initializers in ambient functions
263        // Ambient functions (with 'declare' modifier) cannot have default parameter values
264        let has_declare = self
265            .ctx
266            .arena
267            .has_modifier(&func.modifiers, tsz_scanner::SyntaxKind::DeclareKeyword);
268
269        if has_declare && !func.parameters.nodes.is_empty() {
270            for &param_idx in &func.parameters.nodes {
271                let Some(param_node) = self.ctx.arena.get(param_idx) else {
272                    continue;
273                };
274                let Some(param) = self.ctx.arena.get_parameter(param_node) else {
275                    continue;
276                };
277
278                // If parameter has an initializer in an ambient function, emit TS2371
279                // TSC anchors the error at the parameter name, not the whole parameter.
280                if param.initializer.is_some() {
281                    let name_node = self.ctx.arena.get(param.name).unwrap_or(param_node);
282                    self.ctx.error(
283                        name_node.pos,
284                        name_node.end - name_node.pos,
285                        "A parameter initializer is only allowed in a function or constructor implementation.".to_string(),
286                        2371, // TS2371
287                    );
288                }
289            }
290        }
291
292        // TS1250/TS1251: Function declarations not allowed inside blocks in strict mode
293        // when targeting ES3 or ES5
294        self.check_strict_mode_function_in_block(func_idx);
295    }
296
297    /// TS1250: "Function declarations are not allowed inside blocks in strict mode when targeting 'ES3' or 'ES5'."
298    /// TS1251: Same, with "Class definitions are automatically in strict mode."
299    fn check_strict_mode_function_in_block(&mut self, func_idx: NodeIndex) {
300        // Only applies when targeting ES5 or lower
301        if !self.ctx.compiler_options.target.is_es5() {
302            return;
303        }
304
305        // Check if the function declaration is inside a block that is NOT a function
306        // body, source file, or module block. Walk up to find the block scope container.
307        let Some(ext) = self.ctx.arena.get_extended(func_idx) else {
308            return;
309        };
310        let parent_idx = ext.parent;
311        let Some(parent_node) = self.ctx.arena.get(parent_idx) else {
312            return;
313        };
314
315        // The parent must be a Block (curly braces)
316        if parent_node.kind != syntax_kind_ext::BLOCK {
317            return;
318        }
319
320        // Now check the Block's parent — if it's a function-like, source file, or module,
321        // then this is a valid position for a function declaration
322        let Some(block_ext) = self.ctx.arena.get_extended(parent_idx) else {
323            return;
324        };
325        let block_parent_idx = block_ext.parent;
326        let Some(block_parent) = self.ctx.arena.get(block_parent_idx) else {
327            return;
328        };
329
330        match block_parent.kind {
331            k if k == syntax_kind_ext::FUNCTION_DECLARATION
332                || k == syntax_kind_ext::FUNCTION_EXPRESSION
333                || k == syntax_kind_ext::ARROW_FUNCTION
334                || k == syntax_kind_ext::METHOD_DECLARATION
335                || k == syntax_kind_ext::CONSTRUCTOR
336                || k == syntax_kind_ext::GET_ACCESSOR
337                || k == syntax_kind_ext::SET_ACCESSOR
338                || k == syntax_kind_ext::SOURCE_FILE
339                || k == syntax_kind_ext::MODULE_DECLARATION
340                || k == syntax_kind_ext::CLASS_STATIC_BLOCK_DECLARATION =>
341            {
342                // Function declaration at a valid scope level, no error
343                return;
344            }
345            _ => {}
346        }
347
348        // The function is inside a block (if/while/for/etc.) — check strict mode
349        let in_class = self.is_inside_class(func_idx);
350        let in_strict = in_class
351            || self.ctx.compiler_options.always_strict
352            || self.has_use_strict_directive(func_idx);
353
354        if !in_strict {
355            return;
356        }
357
358        // TSC anchors the error at the function name, not the whole declaration.
359        use crate::diagnostics::{diagnostic_codes, diagnostic_messages};
360        let error_node = self
361            .ctx
362            .arena
363            .get(func_idx)
364            .and_then(|n| self.ctx.arena.get_function(n))
365            .map(|f| f.name)
366            .filter(|n| n.is_some())
367            .unwrap_or(func_idx);
368        let (pos, len) = self
369            .ctx
370            .arena
371            .get(error_node)
372            .map_or((0, 0), |n| (n.pos, n.end - n.pos));
373        if in_class {
374            self.ctx.error(
375                pos,
376                len,
377                diagnostic_messages::FUNCTION_DECLARATIONS_ARE_NOT_ALLOWED_INSIDE_BLOCKS_IN_STRICT_MODE_WHEN_TARGETIN_2
378                    .to_string(),
379                diagnostic_codes::FUNCTION_DECLARATIONS_ARE_NOT_ALLOWED_INSIDE_BLOCKS_IN_STRICT_MODE_WHEN_TARGETIN_2,
380            );
381        } else {
382            self.ctx.error(
383                pos,
384                len,
385                diagnostic_messages::FUNCTION_DECLARATIONS_ARE_NOT_ALLOWED_INSIDE_BLOCKS_IN_STRICT_MODE_WHEN_TARGETIN
386                    .to_string(),
387                diagnostic_codes::FUNCTION_DECLARATIONS_ARE_NOT_ALLOWED_INSIDE_BLOCKS_IN_STRICT_MODE_WHEN_TARGETIN,
388            );
389        }
390    }
391
392    /// Check if a node is inside a class definition (which is always strict mode).
393    fn is_inside_class(&self, node_idx: NodeIndex) -> bool {
394        let mut current = node_idx;
395        while let Some(ext) = self.ctx.arena.get_extended(current) {
396            let parent_idx = ext.parent;
397            if parent_idx.is_none() {
398                return false;
399            }
400            let Some(parent) = self.ctx.arena.get(parent_idx) else {
401                return false;
402            };
403            if parent.kind == syntax_kind_ext::CLASS_DECLARATION
404                || parent.kind == syntax_kind_ext::CLASS_EXPRESSION
405            {
406                return true;
407            }
408            current = parent_idx;
409        }
410        false
411    }
412
413    /// Check if a "use strict" directive is in effect for a node by walking up to
414    /// the nearest function or source file and checking for a "use strict" prologue.
415    fn has_use_strict_directive(&self, node_idx: NodeIndex) -> bool {
416        let mut current = node_idx;
417        while let Some(ext) = self.ctx.arena.get_extended(current) {
418            let parent_idx = ext.parent;
419            if parent_idx.is_none() {
420                return false;
421            }
422            let Some(parent) = self.ctx.arena.get(parent_idx) else {
423                return false;
424            };
425
426            // Check function bodies and source files for "use strict"
427            match parent.kind {
428                k if k == syntax_kind_ext::SOURCE_FILE => {
429                    return self.source_file_has_use_strict(parent_idx);
430                }
431                k if k == syntax_kind_ext::FUNCTION_DECLARATION
432                    || k == syntax_kind_ext::FUNCTION_EXPRESSION
433                    || k == syntax_kind_ext::ARROW_FUNCTION =>
434                {
435                    if let Some(func) = self.ctx.arena.get_function(parent)
436                        && self.block_has_use_strict(func.body)
437                    {
438                        return true;
439                    }
440                    // Continue walking up — the outer scope might have "use strict"
441                }
442                _ => {}
443            }
444            current = parent_idx;
445        }
446        false
447    }
448
449    /// Check if a source file starts with "use strict"
450    fn source_file_has_use_strict(&self, sf_idx: NodeIndex) -> bool {
451        let Some(node) = self.ctx.arena.get(sf_idx) else {
452            return false;
453        };
454        let Some(sf) = self.ctx.arena.get_source_file(node) else {
455            return false;
456        };
457        self.statements_have_use_strict(&sf.statements.nodes)
458    }
459
460    /// Check if a block node starts with "use strict"
461    fn block_has_use_strict(&self, block_idx: NodeIndex) -> bool {
462        let Some(node) = self.ctx.arena.get(block_idx) else {
463            return false;
464        };
465        let Some(block) = self.ctx.arena.get_block(node) else {
466            return false;
467        };
468        self.statements_have_use_strict(&block.statements.nodes)
469    }
470
471    /// Check if a list of statements starts with a "use strict" expression statement
472    fn statements_have_use_strict(&self, stmts: &[NodeIndex]) -> bool {
473        for &stmt_idx in stmts {
474            let Some(stmt) = self.ctx.arena.get(stmt_idx) else {
475                continue;
476            };
477            if stmt.kind != syntax_kind_ext::EXPRESSION_STATEMENT {
478                break; // Prologues must be at the top
479            }
480            let Some(expr_stmt) = self.ctx.arena.get_expression_statement(stmt) else {
481                break;
482            };
483            let Some(expr) = self.ctx.arena.get(expr_stmt.expression) else {
484                break;
485            };
486            if expr.kind == SyntaxKind::StringLiteral as u16 {
487                if let Some(lit) = self.ctx.arena.get_literal(expr)
488                    && lit.text == "use strict"
489                {
490                    return true;
491                }
492            } else {
493                break; // Non-string expression, stop looking for prologues
494            }
495        }
496        false
497    }
498
499    /// Check a class declaration.
500    ///
501    /// Handles declaration-specific class checks including TS2564 (strict property initialization).
502    pub fn check_class_declaration(&mut self, class_idx: NodeIndex) {
503        let Some(node) = self.ctx.arena.get(class_idx) else {
504            return;
505        };
506        let Some(class_decl) = self.ctx.arena.get_class(node) else {
507            return;
508        };
509
510        // Check strict property initialization (TS2564)
511        self.check_property_initialization(class_idx, class_decl);
512    }
513
514    /// Check property initialization for TS2564.
515    ///
516    /// Reports errors for class properties that:
517    /// - Don't have initializers
518    /// - Don't have definite assignment assertions (!)
519    /// - Are not assigned in all constructor code paths
520    const fn check_property_initialization(
521        &mut self,
522        _class_idx: NodeIndex,
523        _class_decl: &tsz_parser::parser::node::ClassData,
524    ) {
525        // Canonical TS2564/TS2454 definite-assignment engine lives in
526        // `CheckerState` + `query_boundaries::definite_assignment`.
527        // DeclarationChecker class checks intentionally delegate through that path
528        // in the normal checker pipeline to avoid duplicate algorithms.
529    }
530
531    /// Check an interface declaration.
532    pub const fn check_interface_declaration(&mut self, _iface_idx: NodeIndex) {
533        // Interface declaration checking is handled by CheckerState for now
534        // Will be migrated incrementally
535        // Key checks:
536        // - Heritage clauses
537        // - Member signatures
538    }
539
540    /// Check a type alias declaration.
541    pub const fn check_type_alias_declaration(&mut self, _alias_idx: NodeIndex) {
542        // Type alias checking is handled by CheckerState for now
543        // Will be migrated incrementally
544        // Key checks:
545        // - Type parameters
546        // - Circular reference detection
547    }
548
549    /// Check an enum declaration.
550    pub fn check_enum_declaration(&mut self, enum_idx: NodeIndex) {
551        use crate::diagnostics::diagnostic_codes;
552        use tsz_parser::parser::node::NodeAccess;
553        use tsz_scanner::SyntaxKind;
554
555        let Some(node) = self.ctx.arena.get(enum_idx) else {
556            return;
557        };
558
559        let Some(enum_data) = self.ctx.arena.get_enum(node) else {
560            return;
561        };
562
563        // TS2431: Enum name cannot be '{0}'.
564        if let Some(name_text) = self.ctx.arena.get_identifier_text(enum_data.name) {
565            match name_text {
566                "any" | "unknown" | "never" | "number" | "bigint" | "boolean" | "string"
567                | "symbol" | "void" | "object" | "undefined" => {
568                    let name_node = self.ctx.arena.get(enum_data.name).unwrap();
569                    self.ctx.error(
570                        name_node.pos,
571                        name_node.end - name_node.pos,
572                        format!("Enum name cannot be '{name_text}'."),
573                        diagnostic_codes::ENUM_NAME_CANNOT_BE,
574                    );
575                }
576                _ => {}
577            }
578        }
579
580        // TS2452: An enum member cannot have a numeric name
581        for &member_idx in &enum_data.members.nodes {
582            if let Some(member_node) = self.ctx.arena.get(member_idx)
583                && let Some(member_data) = self.ctx.arena.get_enum_member(member_node)
584                && let Some(name_node) = self.ctx.arena.get(member_data.name)
585                && (name_node.kind == SyntaxKind::NumericLiteral as u16
586                    || name_node.kind == SyntaxKind::BigIntLiteral as u16)
587            {
588                self.ctx.error(
589                    name_node.pos,
590                    name_node.end - name_node.pos,
591                    "An enum member cannot have a numeric name.".to_string(),
592                    diagnostic_codes::AN_ENUM_MEMBER_CANNOT_HAVE_A_NUMERIC_NAME,
593                );
594            }
595        }
596
597        // TS1066: In ambient enum declarations, member initializer must be constant expression
598        let is_ambient = self
599            .ctx
600            .arena
601            .has_modifier(&enum_data.modifiers, SyntaxKind::DeclareKeyword);
602
603        if is_ambient {
604            // Check each member's initializer
605            for &member_idx in &enum_data.members.nodes {
606                if let Some(member_node) = self.ctx.arena.get(member_idx)
607                    && let Some(member_data) = self.ctx.arena.get_enum_member(member_node)
608                    && member_data.initializer.is_some()
609                {
610                    // Check if the initializer is a constant expression
611                    if !self.is_constant_expression(member_data.initializer)
612                        && let Some(init_node) = self.ctx.arena.get(member_data.initializer)
613                    {
614                        self.ctx.error(
615                                init_node.pos,
616                                init_node.end - init_node.pos,
617                                "In ambient enum declarations member initializer must be constant expression.".to_string(),
618                                diagnostic_codes::IN_AMBIENT_ENUM_DECLARATIONS_MEMBER_INITIALIZER_MUST_BE_CONSTANT_EXPRESSION,
619                            );
620                    }
621                }
622            }
623        }
624
625        // TS1061: Enum member must have initializer
626        let mut auto_incrementable = true;
627        for &member_idx in &enum_data.members.nodes {
628            if let Some(member_node) = self.ctx.arena.get(member_idx)
629                && let Some(member_data) = self.ctx.arena.get_enum_member(member_node)
630            {
631                if member_data.initializer.is_none() {
632                    if !auto_incrementable {
633                        let name_node = self.ctx.arena.get(member_data.name).unwrap_or(member_node);
634                        self.ctx.error(
635                            name_node.pos,
636                            name_node.end - name_node.pos,
637                            "Enum member must have initializer.".to_string(),
638                            diagnostic_codes::ENUM_MEMBER_MUST_HAVE_INITIALIZER,
639                        );
640                    }
641                    auto_incrementable = true;
642                } else {
643                    auto_incrementable =
644                        self.is_numeric_constant_enum_expr(member_data.initializer, enum_data, 0);
645                }
646
647                // TS2565: check for property used before being assigned
648                if member_data.initializer.is_some()
649                    && let Some(member_name) = self.ctx.arena.get_identifier_text(member_data.name)
650                {
651                    let enum_name_text = self.ctx.arena.get_identifier_text(enum_data.name);
652                    self.check_enum_member_self_reference(
653                        member_data.initializer,
654                        member_name,
655                        enum_name_text,
656                    );
657                }
658            }
659        }
660    }
661
662    /// Check if an expression is a constant expression for ambient enum members.
663    ///
664    /// Constant expressions include:
665    /// - Literals (numeric, string, boolean, null)
666    /// - Identifier references (to other enum members or constants)
667    /// - Unary expressions (+, -, ~) on constant expressions
668    /// - Binary expressions on constant expressions
669    /// - Parenthesized constant expressions
670    ///
671    /// Property access expressions like 'foo'.length are NOT constant.
672    fn is_constant_expression(&self, expr_idx: NodeIndex) -> bool {
673        if expr_idx.is_none() {
674            return true;
675        }
676
677        let Some(node) = self.ctx.arena.get(expr_idx) else {
678            return false;
679        };
680
681        use tsz_scanner::SyntaxKind;
682
683        match node.kind {
684            // Literals are always constant
685            k if k == SyntaxKind::NumericLiteral as u16 => true,
686            k if k == SyntaxKind::StringLiteral as u16 => true,
687            k if k == SyntaxKind::NoSubstitutionTemplateLiteral as u16 => true,
688            k if k == SyntaxKind::TrueKeyword as u16 => true,
689            k if k == SyntaxKind::FalseKeyword as u16 => true,
690            k if k == SyntaxKind::NullKeyword as u16 => true,
691
692            // Identifiers (enum member references) are constant
693            k if k == SyntaxKind::Identifier as u16 => true,
694
695            // Template expressions
696            k if k == tsz_parser::parser::syntax_kind_ext::TEMPLATE_EXPRESSION => {
697                if let Some(template) = self.ctx.arena.get_template_expr(node) {
698                    for &span_idx in &template.template_spans.nodes {
699                        if let Some(span_node) = self.ctx.arena.get(span_idx)
700                            && let Some(span) = self.ctx.arena.get_template_span(span_node)
701                            && !self.is_constant_expression(span.expression)
702                        {
703                            return false;
704                        }
705                    }
706                    true
707                } else {
708                    false
709                }
710            }
711
712            // Default case: check using accessor methods
713            _ => {
714                // Unary expressions: +x, -x, ~x
715                if let Some(unary) = self.ctx.arena.get_unary_expr(node) {
716                    return self.is_constant_expression(unary.operand);
717                }
718
719                // Binary expressions: x + y, x * y, etc.
720                if let Some(binary) = self.ctx.arena.get_binary_expr(node) {
721                    return self.is_constant_expression(binary.left)
722                        && self.is_constant_expression(binary.right);
723                }
724
725                // Parenthesized expressions
726                if let Some(paren) = self.ctx.arena.get_parenthesized(node) {
727                    return self.is_constant_expression(paren.expression);
728                }
729
730                // Everything else (including property access) is not constant
731                false
732            }
733        }
734    }
735
736    fn is_numeric_constant_enum_expr(
737        &self,
738        expr_idx: NodeIndex,
739        enum_data: &tsz_parser::parser::node::EnumData,
740        depth: u32,
741    ) -> bool {
742        if depth > 100 {
743            return false;
744        }
745        if expr_idx.is_none() {
746            return true;
747        }
748
749        let Some(node) = self.ctx.arena.get(expr_idx) else {
750            return false;
751        };
752
753        use tsz_parser::parser::node::NodeAccess;
754        use tsz_parser::parser::syntax_kind_ext;
755        use tsz_scanner::SyntaxKind;
756
757        match node.kind {
758            k if k == SyntaxKind::NumericLiteral as u16 => true,
759            k if k == SyntaxKind::Identifier as u16 => {
760                if let Some(name_text) = self.ctx.arena.get_identifier_text(expr_idx) {
761                    for &member_idx in &enum_data.members.nodes {
762                        if let Some(member_node) = self.ctx.arena.get(member_idx)
763                            && let Some(member_data) = self.ctx.arena.get_enum_member(member_node)
764                            && let Some(member_name_text) =
765                                self.ctx.arena.get_identifier_text(member_data.name)
766                            && member_name_text == name_text
767                        {
768                            if member_data.initializer.is_none() {
769                                return true;
770                            } else {
771                                return self.is_numeric_constant_enum_expr(
772                                    member_data.initializer,
773                                    enum_data,
774                                    depth + 1,
775                                );
776                            }
777                        }
778                    }
779                }
780                false
781            }
782            k if k == syntax_kind_ext::PREFIX_UNARY_EXPRESSION => {
783                if let Some(unary) = self.ctx.arena.get_unary_expr(node) {
784                    self.is_numeric_constant_enum_expr(unary.operand, enum_data, depth + 1)
785                } else {
786                    false
787                }
788            }
789            k if k == syntax_kind_ext::BINARY_EXPRESSION => {
790                if let Some(binary) = self.ctx.arena.get_binary_expr(node) {
791                    self.is_numeric_constant_enum_expr(binary.left, enum_data, depth + 1)
792                        && self.is_numeric_constant_enum_expr(binary.right, enum_data, depth + 1)
793                } else {
794                    false
795                }
796            }
797            k if k == syntax_kind_ext::PARENTHESIZED_EXPRESSION => {
798                if let Some(paren) = self.ctx.arena.get_parenthesized(node) {
799                    self.is_numeric_constant_enum_expr(paren.expression, enum_data, depth + 1)
800                } else {
801                    false
802                }
803            }
804            _ => false,
805        }
806    }
807
808    // Module/namespace declaration checking is in `declarations_module.rs`.
809    // Module resolution helpers are in `declarations_module_helpers.rs`.
810
811    /// Check a statement inside an ambient context (declare namespace/module).
812    /// Emits TS1036 for non-declaration statements, plus specific errors for
813    /// continue (TS1104), return (TS1108), and with (TS2410).
814    pub(crate) fn check_statement_in_ambient_context(&mut self, stmt_idx: NodeIndex) {
815        let Some(node) = self.ctx.arena.get(stmt_idx) else {
816            return;
817        };
818
819        // Non-declaration statements are not allowed in ambient contexts
820        let is_non_declaration = matches!(
821            node.kind,
822            k if k == syntax_kind_ext::EXPRESSION_STATEMENT
823                || k == syntax_kind_ext::IF_STATEMENT
824                || k == syntax_kind_ext::DO_STATEMENT
825                || k == syntax_kind_ext::WHILE_STATEMENT
826                || k == syntax_kind_ext::FOR_STATEMENT
827                || k == syntax_kind_ext::FOR_IN_STATEMENT
828                || k == syntax_kind_ext::FOR_OF_STATEMENT
829                || k == syntax_kind_ext::BREAK_STATEMENT
830                || k == syntax_kind_ext::CONTINUE_STATEMENT
831                || k == syntax_kind_ext::RETURN_STATEMENT
832                || k == syntax_kind_ext::WITH_STATEMENT
833                || k == syntax_kind_ext::SWITCH_STATEMENT
834                || k == syntax_kind_ext::THROW_STATEMENT
835                || k == syntax_kind_ext::TRY_STATEMENT
836                || k == syntax_kind_ext::DEBUGGER_STATEMENT
837                || k == syntax_kind_ext::EMPTY_STATEMENT
838        );
839
840        if is_non_declaration {
841            use crate::diagnostics::{diagnostic_codes, diagnostic_messages};
842            if let Some((pos, end)) = self.ctx.get_node_span(stmt_idx) {
843                self.ctx.error(
844                    pos,
845                    end - pos,
846                    diagnostic_messages::STATEMENTS_ARE_NOT_ALLOWED_IN_AMBIENT_CONTEXTS.to_string(),
847                    diagnostic_codes::STATEMENTS_ARE_NOT_ALLOWED_IN_AMBIENT_CONTEXTS,
848                );
849            }
850        }
851
852        // Additional specific checks for certain statements
853        if node.kind == syntax_kind_ext::CONTINUE_STATEMENT {
854            use crate::diagnostics::{diagnostic_codes, diagnostic_messages};
855            if let Some((pos, end)) = self.ctx.get_node_span(stmt_idx) {
856                self.ctx.error(
857                    pos,
858                    end - pos,
859                    diagnostic_messages::A_CONTINUE_STATEMENT_CAN_ONLY_BE_USED_WITHIN_AN_ENCLOSING_ITERATION_STATEMENT.to_string(),
860                    diagnostic_codes::A_CONTINUE_STATEMENT_CAN_ONLY_BE_USED_WITHIN_AN_ENCLOSING_ITERATION_STATEMENT,
861                );
862            }
863        }
864
865        if node.kind == syntax_kind_ext::RETURN_STATEMENT {
866            use crate::diagnostics::{diagnostic_codes, diagnostic_messages};
867            if let Some((pos, end)) = self.ctx.get_node_span(stmt_idx) {
868                self.ctx.error(
869                    pos,
870                    end - pos,
871                    diagnostic_messages::A_RETURN_STATEMENT_CAN_ONLY_BE_USED_WITHIN_A_FUNCTION_BODY
872                        .to_string(),
873                    diagnostic_codes::A_RETURN_STATEMENT_CAN_ONLY_BE_USED_WITHIN_A_FUNCTION_BODY,
874                );
875            }
876        }
877
878        if node.kind == syntax_kind_ext::WITH_STATEMENT {
879            self.check_with_statement(stmt_idx);
880        }
881
882        // Ambient declarations still need index-signature parameter validation (TS1268).
883        if node.kind == syntax_kind_ext::VARIABLE_STATEMENT {
884            self.check_ambient_variable_type_annotations_for_index_signatures(stmt_idx);
885        }
886
887        // Check labeled statements — the inner statement should also be checked
888        if node.kind == syntax_kind_ext::LABELED_STATEMENT
889            && let Some(labeled) = self.ctx.arena.get_labeled_statement(node)
890        {
891            self.check_label_on_declaration(labeled.label, labeled.statement);
892            self.check_statement_in_ambient_context(labeled.statement);
893        }
894    }
895
896    fn check_ambient_variable_type_annotations_for_index_signatures(
897        &mut self,
898        stmt_idx: NodeIndex,
899    ) {
900        use crate::diagnostics::{diagnostic_codes, diagnostic_messages};
901
902        let Some(stmt_node) = self.ctx.arena.get(stmt_idx) else {
903            return;
904        };
905        let Some(var_stmt) = self.ctx.arena.get_variable(stmt_node) else {
906            return;
907        };
908
909        for &list_idx in &var_stmt.declarations.nodes {
910            let Some(list_node) = self.ctx.arena.get(list_idx) else {
911                continue;
912            };
913            let Some(decl_list) = self.ctx.arena.get_variable(list_node) else {
914                continue;
915            };
916            for &decl_idx in &decl_list.declarations.nodes {
917                let Some(decl_node) = self.ctx.arena.get(decl_idx) else {
918                    continue;
919                };
920                let Some(var_decl) = self.ctx.arena.get_variable_declaration(decl_node) else {
921                    continue;
922                };
923                if var_decl.type_annotation.is_none() {
924                    continue;
925                }
926                let Some(type_node) = self.ctx.arena.get(var_decl.type_annotation) else {
927                    continue;
928                };
929                if type_node.kind != syntax_kind_ext::TYPE_LITERAL {
930                    continue;
931                }
932                let Some(type_lit) = self.ctx.arena.get_type_literal(type_node) else {
933                    continue;
934                };
935                for &member_idx in &type_lit.members.nodes {
936                    let Some(member_node) = self.ctx.arena.get(member_idx) else {
937                        continue;
938                    };
939                    let Some(index_sig) = self.ctx.arena.get_index_signature(member_node) else {
940                        continue;
941                    };
942                    let Some(&param_idx) = index_sig.parameters.nodes.first() else {
943                        continue;
944                    };
945                    let Some(param_node) = self.ctx.arena.get(param_idx) else {
946                        continue;
947                    };
948                    let Some(param) = self.ctx.arena.get_parameter(param_node) else {
949                        continue;
950                    };
951                    if param.type_annotation.is_none() {
952                        continue;
953                    }
954                    let Some(type_node) = self.ctx.arena.get(param.type_annotation) else {
955                        continue;
956                    };
957                    let is_valid = type_node.kind == tsz_scanner::SyntaxKind::StringKeyword as u16
958                        || type_node.kind == tsz_scanner::SyntaxKind::NumberKeyword as u16
959                        || type_node.kind == tsz_scanner::SyntaxKind::SymbolKeyword as u16
960                        || type_node.kind == syntax_kind_ext::TEMPLATE_LITERAL_TYPE;
961                    if !is_valid && let Some((pos, end)) = self.ctx.get_node_span(param_idx) {
962                        self.ctx.error(
963                            pos,
964                            end - pos,
965                            diagnostic_messages::AN_INDEX_SIGNATURE_PARAMETER_TYPE_MUST_BE_STRING_NUMBER_SYMBOL_OR_A_TEMPLATE_LIT.to_string(),
966                            diagnostic_codes::AN_INDEX_SIGNATURE_PARAMETER_TYPE_MUST_BE_STRING_NUMBER_SYMBOL_OR_A_TEMPLATE_LIT,
967                        );
968                    }
969                }
970            }
971        }
972    }
973
974    fn is_strict_mode_for_node(&self, idx: NodeIndex) -> bool {
975        self.ctx.is_strict_mode_for_node(idx)
976    }
977
978    fn check_with_statement(&mut self, stmt_idx: NodeIndex) {
979        use crate::diagnostics::{diagnostic_codes, diagnostic_messages};
980
981        if let Some((pos, end)) = self.ctx.get_node_span(stmt_idx) {
982            self.ctx.error(
983                pos,
984                end - pos,
985                diagnostic_messages::THE_WITH_STATEMENT_IS_NOT_SUPPORTED_ALL_SYMBOLS_IN_A_WITH_BLOCK_WILL_HAVE_TYPE_A.to_string(),
986                diagnostic_codes::THE_WITH_STATEMENT_IS_NOT_SUPPORTED_ALL_SYMBOLS_IN_A_WITH_BLOCK_WILL_HAVE_TYPE_A,
987            );
988
989            if self.is_strict_mode_for_node(stmt_idx) {
990                self.ctx.error(
991                    pos,
992                    end - pos,
993                    diagnostic_messages::WITH_STATEMENTS_ARE_NOT_ALLOWED_IN_STRICT_MODE.to_string(),
994                    diagnostic_codes::WITH_STATEMENTS_ARE_NOT_ALLOWED_IN_STRICT_MODE,
995                );
996            }
997        }
998    }
999
1000    fn check_label_on_declaration(&mut self, label_idx: NodeIndex, statement_idx: NodeIndex) {
1001        if !self.ctx.compiler_options.target.supports_es2015() {
1002            return;
1003        }
1004        if !self.is_strict_mode_for_node(label_idx) {
1005            return;
1006        }
1007
1008        let Some(stmt_node) = self.ctx.arena.get(statement_idx) else {
1009            return;
1010        };
1011
1012        let is_declaration_or_variable = matches!(
1013            stmt_node.kind,
1014            syntax_kind_ext::FUNCTION_DECLARATION
1015                | syntax_kind_ext::CLASS_DECLARATION
1016                | syntax_kind_ext::INTERFACE_DECLARATION
1017                | syntax_kind_ext::TYPE_ALIAS_DECLARATION
1018                | syntax_kind_ext::ENUM_DECLARATION
1019                | syntax_kind_ext::MODULE_DECLARATION
1020                | syntax_kind_ext::IMPORT_DECLARATION
1021                | syntax_kind_ext::EXPORT_DECLARATION
1022                | syntax_kind_ext::VARIABLE_STATEMENT
1023        );
1024
1025        if is_declaration_or_variable && let Some((pos, end)) = self.ctx.get_node_span(label_idx) {
1026            self.ctx.error(
1027                pos,
1028                end - pos,
1029                "'A label is not allowed here.".to_string(),
1030                1344, // TS1344
1031            );
1032        }
1033    }
1034
1035    /// Check parameter properties (only valid in constructors).
1036    pub fn check_parameter_properties(&mut self, parameters: &[NodeIndex]) {
1037        use crate::diagnostics::diagnostic_codes;
1038
1039        for &param_idx in parameters {
1040            let Some(node) = self.ctx.arena.get(param_idx) else {
1041                continue;
1042            };
1043
1044            if let Some(param) = self.ctx.arena.get_parameter(node) {
1045                // If parameter has parameter property modifiers (public/private/protected/readonly)
1046                // and we're not in a constructor, report error.
1047                // Decorators on parameters are NOT parameter properties.
1048                let has_prop_modifier = if let Some(ref mods) = param.modifiers {
1049                    mods.nodes.iter().any(|&mod_idx| {
1050                        self.ctx.arena.get(mod_idx).is_some_and(|m| {
1051                            use tsz_scanner::SyntaxKind;
1052                            m.kind == SyntaxKind::PublicKeyword as u16
1053                                || m.kind == SyntaxKind::PrivateKeyword as u16
1054                                || m.kind == SyntaxKind::ProtectedKeyword as u16
1055                                || m.kind == SyntaxKind::ReadonlyKeyword as u16
1056                        })
1057                    })
1058                } else {
1059                    false
1060                };
1061                if has_prop_modifier && let Some((pos, end)) = self.ctx.get_node_span(param_idx) {
1062                    self.ctx.error(
1063                        pos,
1064                        end - pos,
1065                        "A parameter property is only allowed in a constructor implementation."
1066                            .to_string(),
1067                        diagnostic_codes::A_PARAMETER_PROPERTY_IS_ONLY_ALLOWED_IN_A_CONSTRUCTOR_IMPLEMENTATION,
1068                    );
1069                }
1070            }
1071        }
1072    }
1073
1074    /// Check function implementations for overload sequences.
1075    pub const fn check_function_implementations(&mut self, _nodes: &[NodeIndex]) {
1076        // Implementation of overload checking
1077        // Will be migrated from CheckerState
1078    }
1079    fn check_enum_member_self_reference(
1080        &mut self,
1081        expr_idx: NodeIndex,
1082        member_name: &str,
1083        enum_name: Option<&str>,
1084    ) {
1085        if expr_idx.is_none() {
1086            return;
1087        }
1088        let Some(node) = self.ctx.arena.get(expr_idx) else {
1089            return;
1090        };
1091
1092        use crate::diagnostics::diagnostic_codes;
1093        use tsz_parser::parser::node::NodeAccess;
1094        use tsz_parser::parser::syntax_kind_ext;
1095        use tsz_scanner::SyntaxKind;
1096
1097        match node.kind {
1098            k if k == SyntaxKind::Identifier as u16 => {
1099                if let Some(text) = self.ctx.arena.get_identifier_text(expr_idx)
1100                    && text == member_name
1101                {
1102                    self.ctx.error(
1103                        node.pos,
1104                        node.end - node.pos,
1105                        format!("Property '{text}' is used before being assigned."),
1106                        diagnostic_codes::PROPERTY_IS_USED_BEFORE_BEING_ASSIGNED,
1107                    );
1108                }
1109            }
1110            k if k == syntax_kind_ext::PROPERTY_ACCESS_EXPRESSION => {
1111                if let Some(prop) = self.ctx.arena.get_access_expr(node)
1112                    && let Some(left_node) = self.ctx.arena.get(prop.expression)
1113                {
1114                    let is_enum_ref = if left_node.kind == SyntaxKind::Identifier as u16 {
1115                        if let Some(text) = self.ctx.arena.get_identifier_text(prop.expression) {
1116                            Some(text) == enum_name
1117                        } else {
1118                            false
1119                        }
1120                    } else {
1121                        false
1122                    };
1123
1124                    if is_enum_ref {
1125                        if let Some(right_text) =
1126                            self.ctx.arena.get_identifier_text(prop.name_or_argument)
1127                            && right_text == member_name
1128                        {
1129                            self.ctx.error(
1130                                node.pos,
1131                                node.end - node.pos,
1132                                format!("Property '{right_text}' is used before being assigned."),
1133                                diagnostic_codes::PROPERTY_IS_USED_BEFORE_BEING_ASSIGNED,
1134                            );
1135                        }
1136                    } else {
1137                        self.check_enum_member_self_reference(
1138                            prop.expression,
1139                            member_name,
1140                            enum_name,
1141                        );
1142                    }
1143                }
1144            }
1145            k if k == syntax_kind_ext::ELEMENT_ACCESS_EXPRESSION => {
1146                if let Some(elem) = self.ctx.arena.get_access_expr(node)
1147                    && let Some(left_node) = self.ctx.arena.get(elem.expression)
1148                {
1149                    let is_enum_ref = if left_node.kind == SyntaxKind::Identifier as u16 {
1150                        if let Some(text) = self.ctx.arena.get_identifier_text(elem.expression) {
1151                            Some(text) == enum_name
1152                        } else {
1153                            false
1154                        }
1155                    } else {
1156                        false
1157                    };
1158
1159                    if is_enum_ref {
1160                        if let Some(right_node) = self.ctx.arena.get(elem.name_or_argument) {
1161                            if right_node.kind == SyntaxKind::StringLiteral as u16 {
1162                                if let Some(lit) = self.ctx.arena.get_literal(right_node)
1163                                    && lit.text == member_name
1164                                {
1165                                    self.ctx.error(
1166                                        node.pos,
1167                                        node.end - node.pos,
1168                                        format!(
1169                                            "Property '{}' is used before being assigned.",
1170                                            lit.text
1171                                        ),
1172                                        diagnostic_codes::PROPERTY_IS_USED_BEFORE_BEING_ASSIGNED,
1173                                    );
1174                                }
1175                            } else {
1176                                self.check_enum_member_self_reference(
1177                                    elem.name_or_argument,
1178                                    member_name,
1179                                    enum_name,
1180                                );
1181                            }
1182                        }
1183                    } else {
1184                        self.check_enum_member_self_reference(
1185                            elem.expression,
1186                            member_name,
1187                            enum_name,
1188                        );
1189                        self.check_enum_member_self_reference(
1190                            elem.name_or_argument,
1191                            member_name,
1192                            enum_name,
1193                        );
1194                    }
1195                }
1196            }
1197            k if k == syntax_kind_ext::PREFIX_UNARY_EXPRESSION => {
1198                if let Some(unary) = self.ctx.arena.get_unary_expr(node) {
1199                    self.check_enum_member_self_reference(unary.operand, member_name, enum_name);
1200                }
1201            }
1202            k if k == syntax_kind_ext::POSTFIX_UNARY_EXPRESSION => {
1203                if let Some(unary) = self.ctx.arena.get_unary_expr(node) {
1204                    self.check_enum_member_self_reference(unary.operand, member_name, enum_name);
1205                }
1206            }
1207            k if k == syntax_kind_ext::BINARY_EXPRESSION => {
1208                if let Some(bin) = self.ctx.arena.get_binary_expr(node) {
1209                    self.check_enum_member_self_reference(bin.left, member_name, enum_name);
1210                    self.check_enum_member_self_reference(bin.right, member_name, enum_name);
1211                }
1212            }
1213            k if k == syntax_kind_ext::PARENTHESIZED_EXPRESSION => {
1214                if let Some(paren) = self.ctx.arena.get_parenthesized(node) {
1215                    self.check_enum_member_self_reference(paren.expression, member_name, enum_name);
1216                }
1217            }
1218            k if k == syntax_kind_ext::CONDITIONAL_EXPRESSION => {
1219                if let Some(cond) = self.ctx.arena.get_conditional_expr(node) {
1220                    self.check_enum_member_self_reference(cond.condition, member_name, enum_name);
1221                    self.check_enum_member_self_reference(cond.when_true, member_name, enum_name);
1222                    self.check_enum_member_self_reference(cond.when_false, member_name, enum_name);
1223                }
1224            }
1225            _ => {}
1226        }
1227    }
1228}
1229
1230#[cfg(test)]
1231#[path = "../../tests/declarations.rs"]
1232mod tests;