Skip to main content

tsz_checker/flow/
flow_analysis_usage.rs

1//! Flow-based definite assignment and declaration ordering checks.
2
3use std::rc::Rc;
4
5use crate::FlowAnalyzer;
6use crate::diagnostics::Diagnostic;
7use crate::query_boundaries::definite_assignment::should_report_variable_use_before_assignment;
8use crate::state::{CheckerState, MAX_TREE_WALK_ITERATIONS};
9use tsz_binder::SymbolId;
10use tsz_parser::parser::NodeIndex;
11use tsz_parser::parser::node::NodeAccess;
12use tsz_parser::parser::syntax_kind_ext;
13use tsz_scanner::SyntaxKind;
14use tsz_solver::TypeId;
15
16impl<'a> CheckerState<'a> {
17    /// Check flow-aware usage of a variable (definite assignment + type narrowing).
18    ///
19    /// This is the main entry point for flow analysis when variables are used.
20    /// It combines two critical TypeScript features:
21    /// 1. **Definite Assignment Analysis**: Catches use-before-assignment errors
22    /// 2. **Type Narrowing**: Refines types based on control flow
23    ///
24    /// ## Definite Assignment Checking:
25    /// - Block-scoped variables (let/const) without initializers are checked
26    /// - Variables are tracked through all code paths
27    /// - TS2454 error emitted if variable might not be assigned
28    /// - Error: "Variable 'x' is used before being assigned"
29    ///
30    /// ## Type Narrowing:
31    /// - If definitely assigned, applies flow-based type narrowing
32    /// - typeof guards, discriminant checks, null checks refine types
33    /// - Returns narrowed type for precise type checking
34    ///
35    /// ## Rule #42 Integration:
36    /// - If inside a closure and variable is mutable (let/var): Returns declared type
37    /// - If inside a closure and variable is const: Applies narrowing
38    pub fn check_flow_usage(
39        &mut self,
40        idx: NodeIndex,
41        declared_type: TypeId,
42        sym_id: SymbolId,
43    ) -> TypeId {
44        use tracing::trace;
45
46        trace!(?idx, ?declared_type, ?sym_id, "check_flow_usage called");
47
48        // Flow narrowing is only meaningful for variable-like bindings.
49        // Class/function/namespace symbols have stable declared types and
50        // do not participate in definite-assignment analysis.
51        if !self.symbol_participates_in_flow_analysis(sym_id) {
52            trace!("Symbol does not participate in flow analysis, returning declared type");
53            return declared_type;
54        }
55
56        // Const object/array literal bindings have a stable type shape and do not
57        // benefit from control-flow narrowing. Skipping CFG traversal for these
58        // bindings avoids O(N²) reference matching on large call-heavy files.
59        if self.should_skip_flow_narrowing_for_const_literal_binding(sym_id) {
60            return declared_type;
61        }
62
63        // Check definite assignment for block-scoped variables without initializers
64        if should_report_variable_use_before_assignment(self, idx, declared_type, sym_id) {
65            // Report TS2454 error: Variable used before assignment
66            self.emit_definite_assignment_error(idx, sym_id);
67            // Return declared type to avoid cascading errors
68            trace!("Definite assignment error, returning declared type");
69            return declared_type;
70        }
71
72        // Apply type narrowing based on control flow
73        trace!("Applying flow narrowing");
74        let result = self.apply_flow_narrowing(idx, declared_type);
75        trace!(?result, "check_flow_usage result");
76        result
77    }
78
79    fn symbol_participates_in_flow_analysis(&self, sym_id: SymbolId) -> bool {
80        use tsz_binder::symbol_flags;
81
82        self.ctx
83            .binder
84            .get_symbol(sym_id)
85            .is_some_and(|symbol| (symbol.flags & symbol_flags::VARIABLE) != 0)
86    }
87
88    fn should_skip_flow_narrowing_for_const_literal_binding(&self, sym_id: SymbolId) -> bool {
89        let Some(symbol) = self.ctx.binder.get_symbol(sym_id) else {
90            return false;
91        };
92
93        let mut value_decl = symbol.value_declaration;
94        if value_decl.is_none() {
95            return false;
96        }
97
98        let mut decl_node = match self.ctx.arena.get(value_decl) {
99            Some(node) => node,
100            None => return false,
101        };
102
103        // Binder symbols can point at the identifier node for the declaration name.
104        // Normalize to the enclosing VARIABLE_DECLARATION before checking const/init shape.
105        if decl_node.kind == SyntaxKind::Identifier as u16
106            && let Some(ext) = self.ctx.arena.get_extended(value_decl)
107            && ext.parent.is_some()
108            && let Some(parent_node) = self.ctx.arena.get(ext.parent)
109            && parent_node.kind == syntax_kind_ext::VARIABLE_DECLARATION
110        {
111            value_decl = ext.parent;
112            decl_node = parent_node;
113        }
114
115        if decl_node.kind != syntax_kind_ext::VARIABLE_DECLARATION
116            || !self.is_const_variable_declaration(value_decl)
117        {
118            return false;
119        }
120
121        let Some(var_decl) = self.ctx.arena.get_variable_declaration(decl_node) else {
122            return false;
123        };
124        if var_decl.type_annotation.is_some() || var_decl.initializer.is_none() {
125            return false;
126        }
127
128        let Some(init_node) = self.ctx.arena.get(var_decl.initializer) else {
129            return false;
130        };
131        init_node.kind == syntax_kind_ext::OBJECT_LITERAL_EXPRESSION
132            || init_node.kind == syntax_kind_ext::ARRAY_LITERAL_EXPRESSION
133    }
134
135    /// Emit TS2454 error for variable used before definite assignment.
136    fn emit_definite_assignment_error(&mut self, idx: NodeIndex, sym_id: SymbolId) {
137        // Get the location for error reporting and deduplication key
138        let Some(node) = self.ctx.arena.get(idx) else {
139            // If the node doesn't exist in the arena, we can't deduplicate by position
140            // Skip error emission to avoid potential duplicates
141            return;
142        };
143
144        let pos = node.pos;
145
146        // Deduplicate: check if we've already emitted an error for this (node, symbol) pair
147        let key = (pos, sym_id);
148        if !self.ctx.emitted_ts2454_errors.insert(key) {
149            // Already inserted - duplicate error, skip
150            return;
151        }
152
153        // Get the variable name for the error message
154        let name = self
155            .ctx
156            .binder
157            .get_symbol(sym_id)
158            .map_or_else(|| "<unknown>".to_string(), |s| s.escaped_name.clone());
159
160        // Get the location for error reporting
161        let length = node.end - node.pos;
162
163        self.ctx.diagnostics.push(Diagnostic::error(
164            self.ctx.file_name.clone(),
165            pos,
166            length,
167            format!("Variable '{name}' is used before being assigned."),
168            2454, // TS2454
169        ));
170    }
171
172    /// Check if a node is within a parameter's default value initializer.
173    /// This is used to detect `await` used in default parameter values (TS2524).
174    pub(crate) fn is_in_default_parameter(&self, idx: NodeIndex) -> bool {
175        let mut current = idx;
176        let mut iterations = 0;
177        loop {
178            iterations += 1;
179            if iterations > MAX_TREE_WALK_ITERATIONS {
180                return false;
181            }
182            let ext = match self.ctx.arena.get_extended(current) {
183                Some(ext) => ext,
184                None => return false,
185            };
186            let parent_idx = ext.parent;
187            if parent_idx.is_none() {
188                return false;
189            }
190
191            // Check if parent is a parameter and we're in its initializer
192            if let Some(parent_node) = self.ctx.arena.get(parent_idx) {
193                if parent_node.kind == syntax_kind_ext::PARAMETER
194                    && let Some(param) = self.ctx.arena.get_parameter(parent_node)
195                {
196                    // Check if current node is within the initializer
197                    if param.initializer.is_some() {
198                        let init_idx = param.initializer;
199                        // Check if idx is within the initializer subtree
200                        if self.is_node_within(idx, init_idx) {
201                            return true;
202                        }
203                    }
204                }
205                // Stop at function/arrow boundaries - parameters are only at the top level
206                if parent_node.is_function_like() {
207                    return false;
208                }
209            }
210
211            current = parent_idx;
212        }
213    }
214
215    // =========================================================================
216    // Definite Assignment Checking
217    // =========================================================================
218
219    /// Check if definite assignment checking should be skipped for a given type.
220    /// TypeScript skips TS2454 when the declared type is `any`, `unknown`, or includes `undefined`.
221    pub(crate) fn skip_definite_assignment_for_type(&self, declared_type: TypeId) -> bool {
222        use tsz_solver::TypeId;
223        use tsz_solver::type_contains_undefined;
224
225        // Skip for any/unknown/error - these types allow uninitialized usage
226        if declared_type == TypeId::ANY
227            || declared_type == TypeId::UNKNOWN
228            || declared_type == TypeId::ERROR
229        {
230            return true;
231        }
232
233        // Skip if the type includes undefined or void (uninitialized variables are undefined)
234        type_contains_undefined(self.ctx.types, declared_type)
235    }
236
237    /// - Not in ambient contexts
238    /// - Not in type-only positions
239    pub(crate) fn should_check_definite_assignment(
240        &mut self,
241        sym_id: SymbolId,
242        idx: NodeIndex,
243    ) -> bool {
244        use tsz_binder::symbol_flags;
245        use tsz_parser::parser::node::NodeAccess;
246
247        // TS2454 is only emitted under strictNullChecks (matches tsc behavior)
248        if !self.ctx.strict_null_checks() {
249            return false;
250        }
251
252        // Skip definite assignment check if this identifier is a for-in/for-of
253        // initializer — it's an assignment target, not a usage.
254        // e.g., `let x: number; for (x of items) { ... }` — the `x` in `for (x of ...)`
255        // is being written to, not read from.
256        if self.is_for_in_of_initializer(idx) {
257            return false;
258        }
259
260        // Skip definite assignment check if this identifier is an assignment target
261        // in a destructuring assignment — it's being written to, not read.
262        // e.g., `let x: string; [x] = items;` — the `x` is being assigned to.
263        if self.is_destructuring_assignment_target(idx) {
264            return false;
265        }
266
267        // Get the symbol
268        let Some(symbol) = self.ctx.binder.get_symbol(sym_id) else {
269            return false;
270        };
271
272        // Flow analysis operates within a single file's AST.
273        // We cannot prove assignment across files, so we assume it was assigned.
274        if symbol.decl_file_idx != u32::MAX
275            && symbol.decl_file_idx != self.ctx.current_file_idx as u32
276        {
277            return false;
278        }
279
280        // Check both block-scoped (let/const) and function-scoped (var) variables.
281        // Parameters are excluded downstream (PARAMETER nodes ≠ VARIABLE_DECLARATION).
282        if (symbol.flags & symbol_flags::VARIABLE) == 0 {
283            return false;
284        }
285
286        // Get the value declaration
287        let decl_id = symbol.value_declaration;
288        if decl_id.is_none() {
289            return false;
290        }
291
292        // Get the declaration node
293        let Some(decl_node) = self.ctx.arena.get(decl_id) else {
294            return false;
295        };
296
297        let mut decl_node = decl_node;
298        let mut decl_id_to_check = decl_id;
299        if decl_node.kind == tsz_scanner::SyntaxKind::Identifier as u16
300            && let Some(info) = self.ctx.arena.node_info(decl_id)
301            && let Some(parent) = self.ctx.arena.get(info.parent)
302        {
303            decl_node = parent;
304            decl_id_to_check = info.parent;
305        }
306
307        // If the declaration is a binding element that is ultimately a parameter,
308        // we should not perform definite assignment checking. Parameters are
309        // always definitely assigned.
310        if decl_node.kind == syntax_kind_ext::BINDING_ELEMENT {
311            let mut current = decl_id_to_check;
312            for _ in 0..10 {
313                if let Some(info) = self.ctx.arena.node_info(current) {
314                    let parent = info.parent;
315                    if let Some(parent_node) = self.ctx.arena.get(parent)
316                        && parent_node.kind == syntax_kind_ext::PARAMETER
317                    {
318                        return false;
319                    }
320                    current = parent;
321                } else {
322                    break;
323                }
324            }
325        }
326        let (has_initializer, has_exclamation) =
327            if decl_node.kind == syntax_kind_ext::VARIABLE_DECLARATION {
328                let Some(var_data) = self.ctx.arena.get_variable_declaration(decl_node) else {
329                    return false;
330                };
331                (var_data.initializer.is_some(), var_data.exclamation_token)
332            } else if decl_node.kind == syntax_kind_ext::BINDING_ELEMENT {
333                let Some(_var_data) = self.ctx.arena.get_binding_element(decl_node) else {
334                    return false;
335                };
336                (true, false)
337            } else {
338                return false;
339            };
340
341        // If there's an initializer, skip definite assignment check — unless the variable
342        // is `var` (function-scoped) and the usage is before the declaration in source
343        // order.  `var` hoists the binding but NOT the initializer, so at the usage
344        // point the variable is `undefined`.  Block-scoped variables (let/const) don't
345        // need this: TDZ checks handle pre-declaration use separately.
346        if has_initializer {
347            let is_function_scoped =
348                symbol.flags & tsz_binder::symbol_flags::FUNCTION_SCOPED_VARIABLE != 0;
349
350            if let Some(usage_node) = self.ctx.arena.get(idx)
351                && should_skip_daa_for_initialized_function_scoped_var(
352                    is_function_scoped,
353                    self.is_source_file_global_var_decl(decl_id_to_check),
354                    self.enclosing_top_level_statement_kind(decl_id_to_check),
355                    usage_node.pos,
356                    decl_node.end,
357                )
358            {
359                return false;
360            }
361
362            if !is_function_scoped {
363                return false;
364            }
365
366            if let Some(_usage_node) = self.ctx.arena.get(idx) {
367                // Check if usage is textually inside the initializer expression of the variable.
368                // e.g. var x = f(() => x); // `x` inside is used before assignment completes!
369                // However, general usages after the declaration shouldn't be skipped just because of position,
370                // as they could be in a catch block or after a conditional return.
371                // The control flow graph should be the ultimate source of truth.
372
373                // If usage is inside a nested function relative to the declaration, skip
374                if self.find_enclosing_function_or_source_file(decl_id_to_check)
375                    != self.find_enclosing_function_or_source_file(idx)
376                {
377                    return false;
378                }
379            }
380        }
381
382        // If there's a definite assignment assertion (!), skip check
383        if has_exclamation {
384            return false;
385        }
386
387        // If the variable is declared in a for-in or for-of loop header,
388        // it's assigned by the loop iteration itself - but only when usage is at or after the loop.
389        // A usage BEFORE the loop in source order (e.g. `v; for (var v of [0]) {}`) must still
390        // be checked for definite assignment.
391        if let Some(decl_list_info) = self.ctx.arena.node_info(decl_id_to_check) {
392            let decl_list_idx = decl_list_info.parent;
393            if let Some(decl_list_node) = self.ctx.arena.get(decl_list_idx)
394                && decl_list_node.kind == syntax_kind_ext::VARIABLE_DECLARATION_LIST
395                && let Some(for_info) = self.ctx.arena.node_info(decl_list_idx)
396            {
397                let for_idx = for_info.parent;
398                if let Some(for_node) = self.ctx.arena.get(for_idx)
399                    && (for_node.kind == syntax_kind_ext::FOR_IN_STATEMENT
400                        || for_node.kind == syntax_kind_ext::FOR_OF_STATEMENT)
401                {
402                    // Only skip the check if the usage is at or after the start of the loop.
403                    // If the usage precedes the loop in source order, fall through to DAA.
404                    if let Some(usage_node) = self.ctx.arena.get(idx) {
405                        if usage_node.pos >= for_node.pos {
406                            return false;
407                        }
408                        // Usage is before the loop - continue to definite assignment check
409                    } else {
410                        return false;
411                    }
412                }
413            }
414        }
415
416        // For source-file globals, skip TS2454 when the usage occurs inside a
417        // function-like body. The variable may be assigned before invocation.
418        if self.is_source_file_global_var_decl(decl_id_to_check)
419            && self.is_inside_function_like(idx)
420        {
421            return false;
422        }
423
424        // For namespace-scoped variables, skip TS2454 when the usage is inside
425        // a nested namespace (MODULE_DECLARATION) relative to the declaration.
426        // Flow analysis can't cross namespace boundaries, and the variable may
427        // be assigned in the outer namespace before the inner namespace executes.
428        // Same-namespace usage still gets TS2454 (flow analysis works within a scope).
429        if self.is_usage_in_nested_namespace_from_decl(decl_id_to_check, idx) {
430            return false;
431        }
432
433        // 1. Skip definite assignment checks in ambient declarations (declare const/let, declare module)
434        if self.is_ambient_declaration(decl_id_to_check) {
435            return false;
436        }
437
438        // 2. Anchor checks to a function-like or source-file container
439        let mut current = decl_id_to_check;
440        let mut found_container_scope = false;
441        for _ in 0..50 {
442            let Some(info) = self.ctx.arena.node_info(current) else {
443                break;
444            };
445            if let Some(node) = self.ctx.arena.get(current) {
446                // Check if we're inside a function-like or source-file container scope
447                if node.is_function_like() || node.kind == syntax_kind_ext::SOURCE_FILE {
448                    found_container_scope = true;
449                    break;
450                }
451            }
452
453            current = info.parent;
454            if current.is_none() {
455                break;
456            }
457        }
458
459        // Only check definite assignment when we can anchor to a container scope.
460        found_container_scope
461    }
462
463    fn is_source_file_global_var_decl(&self, decl_id: NodeIndex) -> bool {
464        let Some(info) = self.ctx.arena.node_info(decl_id) else {
465            return false;
466        };
467        let mut current = info.parent;
468        for _ in 0..50 {
469            let Some(node) = self.ctx.arena.get(current) else {
470                return false;
471            };
472            if node.kind == syntax_kind_ext::SOURCE_FILE {
473                return true;
474            }
475            if node.is_function_like() {
476                return false;
477            }
478            let Some(next) = self.ctx.arena.node_info(current).map(|n| n.parent) else {
479                return false;
480            };
481            current = next;
482            if current.is_none() {
483                return false;
484            }
485        }
486        false
487    }
488
489    fn enclosing_top_level_statement_kind(&self, node_idx: NodeIndex) -> Option<u16> {
490        let mut current = node_idx;
491        for _ in 0..50 {
492            let info = self.ctx.arena.node_info(current)?;
493            let parent = info.parent;
494            let parent_node = self.ctx.arena.get(parent)?;
495            if parent_node.kind == syntax_kind_ext::SOURCE_FILE {
496                return self.ctx.arena.get(current).map(|n| n.kind);
497            }
498            current = parent;
499            if current.is_none() {
500                return None;
501            }
502        }
503        None
504    }
505
506    fn is_inside_function_like(&self, idx: NodeIndex) -> bool {
507        let mut current = idx;
508        for _ in 0..50 {
509            let Some(info) = self.ctx.arena.node_info(current) else {
510                return false;
511            };
512            current = info.parent;
513            let Some(node) = self.ctx.arena.get(current) else {
514                return false;
515            };
516            if node.is_function_like() {
517                return true;
518            }
519            if node.kind == syntax_kind_ext::SOURCE_FILE {
520                return false;
521            }
522        }
523        false
524    }
525
526    /// Check if a usage crosses a namespace boundary relative to its declaration.
527    /// Walk up from the usage node; if we encounter a `MODULE_DECLARATION` before
528    /// reaching the node that contains the declaration, the usage is in a nested
529    /// namespace and TS2454 should be suppressed (flow graph doesn't span across
530    /// namespace boundaries).
531    fn is_usage_in_nested_namespace_from_decl(
532        &self,
533        decl_id: NodeIndex,
534        usage_idx: NodeIndex,
535    ) -> bool {
536        let Some(decl_node) = self.ctx.arena.get(decl_id) else {
537            return false;
538        };
539        let decl_pos = decl_node.pos;
540        let decl_end = decl_node.end;
541
542        let mut current = usage_idx;
543        for _ in 0..50 {
544            let Some(info) = self.ctx.arena.node_info(current) else {
545                break;
546            };
547            current = info.parent;
548            let Some(node) = self.ctx.arena.get(current) else {
549                break;
550            };
551            // If this node's span contains the declaration, we've reached the
552            // common container — no namespace boundary between usage and decl.
553            if node.pos <= decl_pos && node.end >= decl_end {
554                return false;
555            }
556            // Hit a MODULE_DECLARATION before reaching the declaration's container:
557            // usage is in a nested namespace.
558            if node.kind == syntax_kind_ext::MODULE_DECLARATION {
559                return true;
560            }
561            if current.is_none() {
562                break;
563            }
564        }
565        false
566    }
567
568    /// Check if a node is a for-in/for-of initializer (assignment target).
569    /// For `for (x of items)`, the identifier `x` is the initializer and is
570    /// being assigned to, not read from.
571    fn is_for_in_of_initializer(&self, idx: NodeIndex) -> bool {
572        use tsz_parser::parser::node::NodeAccess;
573
574        let Some(info) = self.ctx.arena.node_info(idx) else {
575            return false;
576        };
577        let parent = info.parent;
578        let Some(parent_node) = self.ctx.arena.get(parent) else {
579            return false;
580        };
581        if (parent_node.kind == syntax_kind_ext::FOR_IN_STATEMENT
582            || parent_node.kind == syntax_kind_ext::FOR_OF_STATEMENT)
583            && let Some(for_data) = self.ctx.arena.get_for_in_of(parent_node)
584            && for_data.initializer == idx
585        {
586            return true;
587        }
588        false
589    }
590
591    /// Check if an identifier is an assignment target in a destructuring assignment.
592    /// e.g., `[x] = a` or `({x} = a)` — the `x` is being written to, not read.
593    fn is_destructuring_assignment_target(&self, idx: NodeIndex) -> bool {
594        let mut current = idx;
595        for _ in 0..10 {
596            let Some(info) = self.ctx.arena.node_info(current) else {
597                return false;
598            };
599            let parent = info.parent;
600            let Some(parent_node) = self.ctx.arena.get(parent) else {
601                return false;
602            };
603            match parent_node.kind {
604                k if k == syntax_kind_ext::ARRAY_LITERAL_EXPRESSION
605                    || k == syntax_kind_ext::OBJECT_LITERAL_EXPRESSION
606                    || k == syntax_kind_ext::SPREAD_ELEMENT
607                    || k == syntax_kind_ext::SPREAD_ASSIGNMENT
608                    || k == syntax_kind_ext::PROPERTY_ASSIGNMENT
609                    || k == syntax_kind_ext::SHORTHAND_PROPERTY_ASSIGNMENT =>
610                {
611                    current = parent;
612                }
613                k if k == syntax_kind_ext::BINARY_EXPRESSION => {
614                    // Check this is the LHS of a simple assignment (=)
615                    if let Some(bin) = self.ctx.arena.get_binary_expr(parent_node)
616                        && bin.operator_token == SyntaxKind::EqualsToken as u16
617                        && bin.left == current
618                    {
619                        return true;
620                    }
621                    return false;
622                }
623                k if k == syntax_kind_ext::FOR_IN_STATEMENT
624                    || k == syntax_kind_ext::FOR_OF_STATEMENT =>
625                {
626                    if let Some(for_node) = self.ctx.arena.get_for_in_of(parent_node)
627                        && for_node.initializer == current
628                    {
629                        return true;
630                    }
631                    return false;
632                }
633                _ => return false,
634            }
635        }
636        false
637    }
638
639    /// Check if a variable is definitely assigned at a given point.
640    ///
641    /// This performs flow-sensitive analysis to determine if a variable
642    /// has been assigned on all code paths leading to the usage point.
643    pub(crate) fn is_definitely_assigned_at(&self, idx: NodeIndex) -> bool {
644        // Get the flow node for this identifier usage.
645        // Identifier reference nodes (e.g., `a` in `console.log(a)`) typically
646        // don't have direct flow nodes recorded — the binder only records flow
647        // for statements and declarations. Walk up the AST to find the nearest
648        // ancestor with a flow node, mirroring `apply_flow_narrowing`'s fallback.
649        let flow_node = if let Some(flow) = self.ctx.binder.get_node_flow(idx) {
650            flow
651        } else {
652            let mut current = self.ctx.arena.get_extended(idx).map(|ext| ext.parent);
653            let mut found = None;
654            while let Some(parent) = current {
655                if parent.is_none() {
656                    break;
657                }
658                if let Some(flow) = self.ctx.binder.get_node_flow(parent) {
659                    found = Some(flow);
660                    break;
661                }
662                current = self.ctx.arena.get_extended(parent).map(|ext| ext.parent);
663            }
664            match found {
665                Some(flow) => flow,
666                None => {
667                    tracing::debug!("No flow info for {idx:?} or its ancestors");
668                    return true;
669                }
670            }
671        };
672
673        // Create a flow analyzer and check definite assignment
674        let analyzer = FlowAnalyzer::with_node_types(
675            self.ctx.arena,
676            self.ctx.binder,
677            self.ctx.types,
678            &self.ctx.node_types,
679        )
680        .with_flow_cache(&self.ctx.flow_analysis_cache)
681        .with_reference_match_cache(&self.ctx.flow_reference_match_cache)
682        .with_type_environment(Rc::clone(&self.ctx.type_environment));
683
684        analyzer.is_definitely_assigned(idx, flow_node)
685    }
686
687    // =========================================================================
688    // Temporal Dead Zone (TDZ) Checking
689    // =========================================================================
690
691    /// Check if a variable is used before its declaration in a static block.
692    ///
693    /// This detects Temporal Dead Zone (TDZ) violations where a block-scoped variable
694    /// is accessed inside a class static block before it has been declared in the source.
695    ///
696    /// # Example
697    /// ```typescript
698    /// class C {
699    ///   static {
700    ///     console.log(x); // Error: x used before declaration
701    ///   }
702    /// }
703    /// let x = 1;
704    /// ```
705    pub(crate) fn is_variable_used_before_declaration_in_static_block(
706        &self,
707        sym_id: SymbolId,
708        usage_idx: NodeIndex,
709    ) -> bool {
710        use tsz_binder::symbol_flags;
711
712        // 1. Get the symbol
713        let Some(symbol) = self.ctx.binder.symbols.get(sym_id) else {
714            return false;
715        };
716
717        // 2. Check if it is a block-scoped variable (let, const, class, enum)
718        // var and function are hoisted, so they don't have TDZ issues in this context.
719        // Imports (ALIAS) are also hoisted or handled differently.
720        let is_block_scoped = (symbol.flags
721            & (symbol_flags::BLOCK_SCOPED_VARIABLE | symbol_flags::CLASS | symbol_flags::ENUM))
722            != 0;
723
724        if !is_block_scoped {
725            return false;
726        }
727
728        // Skip cross-file symbols — TDZ position comparison only valid within same file
729        if symbol.decl_file_idx != u32::MAX
730            && symbol.decl_file_idx != self.ctx.current_file_idx as u32
731        {
732            return false;
733        }
734
735        // 3. Get the declaration node
736        // Prefer value_declaration, fall back to first declaration
737        let decl_idx = if symbol.value_declaration.is_some() {
738            symbol.value_declaration
739        } else if let Some(&first_decl) = symbol.declarations.first() {
740            first_decl
741        } else {
742            return false;
743        };
744
745        // 4. Check textual order: Usage must be textually before declaration
746        // We ensure both nodes exist in the current arena
747        let Some(usage_node) = self.ctx.arena.get(usage_idx) else {
748            return false;
749        };
750        let Some(decl_node) = self.ctx.arena.get(decl_idx) else {
751            return false;
752        };
753
754        // If usage is after declaration, it's valid
755        if usage_node.pos >= decl_node.end {
756            return false;
757        }
758
759        // For CLASS symbols: if the usage is inside the class body (usage_node.pos > decl_node.pos),
760        // the reference is a self-reference within the class's own static block and is NOT a TDZ
761        // violation. For example, inside `static {}` of class C, referencing C itself is valid.
762        // The class name is accessible within its own body.
763        if symbol.flags & symbol_flags::CLASS != 0 && usage_node.pos > decl_node.pos {
764            return false;
765        }
766
767        // 5. Check if usage is inside a static block
768        // Use find_enclosing_static_block which walks up the AST and stops at function boundaries.
769        // This ensures we only catch immediate usage, not usage inside a closure/function
770        // defined within the static block (which would execute later).
771        if self.find_enclosing_static_block(usage_idx).is_some() {
772            return true;
773        }
774
775        false
776    }
777
778    /// Check if a variable is used before its declaration in a computed property.
779    ///
780    /// Computed property names are evaluated before the property declaration,
781    /// creating a TDZ for the class being declared.
782    pub(crate) fn is_variable_used_before_declaration_in_computed_property(
783        &self,
784        sym_id: SymbolId,
785        usage_idx: NodeIndex,
786    ) -> bool {
787        use tsz_binder::symbol_flags;
788
789        // 1. Get the symbol
790        let Some(symbol) = self.ctx.binder.symbols.get(sym_id) else {
791            return false;
792        };
793
794        // 2. Check if it is a block-scoped variable (let, const, class, enum)
795        let is_block_scoped = (symbol.flags
796            & (symbol_flags::BLOCK_SCOPED_VARIABLE | symbol_flags::CLASS | symbol_flags::ENUM))
797            != 0;
798
799        if !is_block_scoped {
800            return false;
801        }
802
803        // Skip cross-file symbols — TDZ position comparison only valid within same file
804        if symbol.decl_file_idx != u32::MAX
805            && symbol.decl_file_idx != self.ctx.current_file_idx as u32
806        {
807            return false;
808        }
809
810        // 3. Get the declaration node
811        let decl_idx = if symbol.value_declaration.is_some() {
812            symbol.value_declaration
813        } else if let Some(&first_decl) = symbol.declarations.first() {
814            first_decl
815        } else {
816            return false;
817        };
818
819        // 4. Check textual order: Usage must be textually before declaration
820        let Some(usage_node) = self.ctx.arena.get(usage_idx) else {
821            return false;
822        };
823        let Some(decl_node) = self.ctx.arena.get(decl_idx) else {
824            return false;
825        };
826
827        if usage_node.pos >= decl_node.end {
828            return false;
829        }
830
831        // 5. Check if usage is inside a computed property name
832        if self.find_enclosing_computed_property(usage_idx).is_some() {
833            return true;
834        }
835
836        false
837    }
838
839    /// Check if a variable is used before its declaration in a heritage clause.
840    ///
841    /// Heritage clauses (extends, implements) are evaluated before the class body,
842    /// creating a TDZ for the class being declared.
843    pub(crate) fn is_variable_used_before_declaration_in_heritage_clause(
844        &self,
845        sym_id: SymbolId,
846        usage_idx: NodeIndex,
847    ) -> bool {
848        use tsz_binder::symbol_flags;
849
850        // 1. Get the symbol
851        let Some(symbol) = self.ctx.binder.symbols.get(sym_id) else {
852            return false;
853        };
854
855        // 2. Check if it is a block-scoped variable (let, const, class, enum)
856        let is_block_scoped = (symbol.flags
857            & (symbol_flags::BLOCK_SCOPED_VARIABLE | symbol_flags::CLASS | symbol_flags::ENUM))
858            != 0;
859
860        if !is_block_scoped {
861            return false;
862        }
863
864        // Skip TDZ check for type-only contexts (interface extends, type parameters, etc.)
865        // Types are resolved at compile-time, so they don't have temporal dead zones.
866        if self.is_in_type_only_context(usage_idx) {
867            return false;
868        }
869
870        // Skip cross-file symbols — TDZ position comparison only makes sense
871        // within the same file.
872        if symbol.decl_file_idx != u32::MAX
873            && symbol.decl_file_idx != self.ctx.current_file_idx as u32
874        {
875            return false;
876        }
877
878        // 3. Get the declaration node
879        let decl_idx = if symbol.value_declaration.is_some() {
880            symbol.value_declaration
881        } else if let Some(&first_decl) = symbol.declarations.first() {
882            first_decl
883        } else {
884            return false;
885        };
886
887        // 4. Check textual order: Usage must be textually before declaration
888        let Some(usage_node) = self.ctx.arena.get(usage_idx) else {
889            return false;
890        };
891        let Some(decl_node) = self.ctx.arena.get(decl_idx) else {
892            return false;
893        };
894
895        if usage_node.pos >= decl_node.end {
896            return false;
897        }
898
899        // 5. Check if usage is inside a heritage clause (extends/implements)
900        if self.find_enclosing_heritage_clause(usage_idx).is_some() {
901            return true;
902        }
903
904        false
905    }
906
907    /// TS2448/TS2449/TS2450: Check if a block-scoped declaration (class, enum,
908    /// let/const) is used before its declaration in immediately executing code
909    /// (not inside a function/method body).
910    pub(crate) fn is_class_or_enum_used_before_declaration(
911        &self,
912        sym_id: SymbolId,
913        usage_idx: NodeIndex,
914    ) -> bool {
915        use tsz_binder::symbol_flags;
916        use tsz_parser::parser::syntax_kind_ext;
917
918        let Some(symbol) = self.ctx.binder.symbols.get(sym_id) else {
919            return false;
920        };
921
922        // Applies to block-scoped declarations: class, enum, let/const
923        let is_block_scoped = (symbol.flags
924            & (symbol_flags::CLASS | symbol_flags::ENUM | symbol_flags::BLOCK_SCOPED_VARIABLE))
925            != 0;
926        if !is_block_scoped {
927            return false;
928        }
929
930        // Skip TDZ check for type-only contexts (type annotations, typeof in types, etc.)
931        // Types are resolved at compile-time, so they don't have temporal dead zones.
932        if self.is_in_type_only_context(usage_idx) {
933            return false;
934        }
935
936        // Skip check for cross-file symbols (imported from another file).
937        // Position comparison only makes sense within the same file.
938        if symbol.import_module.is_some() {
939            return false;
940        }
941        let is_cross_file = symbol.decl_file_idx != u32::MAX
942            && symbol.decl_file_idx != self.ctx.current_file_idx as u32;
943
944        if is_cross_file && (self.ctx.current_file_idx as u32) > symbol.decl_file_idx {
945            return false;
946        }
947
948        // In multi-file mode, symbol declarations may reference nodes in another
949        // file's arena.  `self.ctx.arena` only contains the *current* file, so
950        // looking up the declaration index would yield an unrelated node whose
951        // position comparison is meaningless.  Detect this by verifying that the
952        // node found at the declaration index really IS a class / enum / variable
953        // declaration — if it isn't, the index came from a different arena.
954        let is_multi_file = self.ctx.all_arenas.is_some();
955
956        // Get the declaration position
957        let decl_idx = if symbol.value_declaration.is_some() {
958            symbol.value_declaration
959        } else if let Some(&first_decl) = symbol.declarations.first() {
960            first_decl
961        } else {
962            return false;
963        };
964
965        let Some(usage_node) = self.ctx.arena.get(usage_idx) else {
966            return false;
967        };
968
969        let mut decl_node_opt = self.ctx.arena.get(decl_idx);
970        let mut decl_arena = self.ctx.arena;
971
972        if is_cross_file
973            && let Some(arenas) = self.ctx.all_arenas.as_ref()
974            && let Some(arena) = arenas.get(symbol.decl_file_idx as usize)
975        {
976            decl_node_opt = arena.get(decl_idx);
977            decl_arena = arena.as_ref();
978        }
979
980        let Some(decl_node) = decl_node_opt else {
981            return false;
982        };
983
984        // In multi-file mode, validate the declaration node kind matches the
985        // symbol.  A mismatch means the node index is from a different file's
986        // arena and should not be compared.
987        if is_multi_file && !is_cross_file {
988            let is_class = symbol.flags & symbol_flags::CLASS != 0;
989            let is_enum = symbol.flags & symbol_flags::ENUM != 0;
990            let is_var = symbol.flags & symbol_flags::BLOCK_SCOPED_VARIABLE != 0;
991            let kind_ok = (is_class
992                && (decl_node.kind == syntax_kind_ext::CLASS_DECLARATION
993                    || decl_node.kind == syntax_kind_ext::CLASS_EXPRESSION))
994                || (is_enum && decl_node.kind == syntax_kind_ext::ENUM_DECLARATION)
995                || (is_var
996                    && (decl_node.kind == syntax_kind_ext::VARIABLE_DECLARATION
997                        || decl_node.kind == syntax_kind_ext::PARAMETER
998                        || decl_node.kind == syntax_kind_ext::BINDING_ELEMENT
999                        || decl_node.kind == tsz_scanner::SyntaxKind::Identifier as u16));
1000            if !kind_ok {
1001                return false;
1002            }
1003        }
1004
1005        // Skip ambient declarations — `declare class`/`declare enum` are type-level
1006        // and have no TDZ. In multi-file mode, search all arenas since decl_idx may
1007        // point to a node in another file's arena.
1008        if is_cross_file {
1009            if let Some(class) = decl_arena.get_class(decl_node)
1010                && self.has_declare_modifier_in_arena(decl_arena, &class.modifiers)
1011            {
1012                return false;
1013            }
1014            if let Some(enum_decl) = decl_arena.get_enum(decl_node)
1015                && self.has_declare_modifier_in_arena(decl_arena, &enum_decl.modifiers)
1016            {
1017                return false;
1018            }
1019        } else if self.is_ambient_declaration(decl_idx) {
1020            return false;
1021        }
1022
1023        // Only flag if usage is before declaration in source order
1024        // EXCEPT for block-scoped variables, which are also in TDZ during their own initializer.
1025        // For classes and enums, usage >= pos is always safe (handled by other specific TDZ checks
1026        // for computed properties, heritage clauses, etc.).
1027        let is_var = symbol.flags & symbol_flags::BLOCK_SCOPED_VARIABLE != 0;
1028        let mut could_be_in_initializer = false;
1029        if !is_cross_file && usage_node.pos >= decl_node.pos {
1030            if is_var && usage_node.pos <= decl_node.end {
1031                // It might be in the initializer. We will confirm via AST walk.
1032                could_be_in_initializer = true;
1033            } else {
1034                return false;
1035            }
1036        }
1037
1038        // Find the declaration's enclosing function-like container (or source file).
1039        // This is the scope that "owns" both the declaration and (potentially) the usage.
1040        let decl_container = if is_cross_file {
1041            None // Walk up to source file
1042        } else {
1043            Some(self.find_enclosing_function_or_source_file(decl_idx))
1044        };
1045
1046        // Walk up from usage: if we hit a function-like boundary BEFORE reaching
1047        // the declaration's container, the usage is in deferred code (a nested
1048        // function/arrow/method) and is NOT a TDZ violation.
1049        // If we reach the declaration's container without crossing a function
1050        // boundary, the usage executes immediately and IS a violation.
1051        let mut current = usage_idx;
1052        let mut found_decl_in_path = false;
1053        while current.is_some() {
1054            let Some(node) = self.ctx.arena.get(current) else {
1055                break;
1056            };
1057            if current == decl_idx {
1058                found_decl_in_path = true;
1059            }
1060            // If we reached the declaration container, stop - same scope means TDZ
1061            if Some(current) == decl_container {
1062                break;
1063            }
1064            // If we reach a function-like boundary before the decl container,
1065            // the usage is deferred and not a TDZ violation.
1066            // Exception: IIFEs (immediately invoked function expressions) execute
1067            // immediately, so they ARE TDZ violations.
1068            if node.is_function_like() && !self.ctx.arena.is_immediately_invoked(current) {
1069                return false;
1070            }
1071            // IIFE - continue walking up, this function executes immediately
1072            // Non-static class property initializers run during constructor execution,
1073            // which is deferred — not a TDZ violation for class declarations.
1074            if node.kind == syntax_kind_ext::PROPERTY_DECLARATION
1075                && let Some(prop) = self.ctx.arena.get_property_decl(node)
1076                && !self.has_static_modifier(&prop.modifiers)
1077            {
1078                return false;
1079            }
1080            // Export assignments (`export = X` / `export default X`) are not TDZ
1081            // violations: the compiler reorders them after all declarations, so
1082            // the referenced class/variable is initialized by the time the export
1083            // binding is created.
1084            if node.kind == syntax_kind_ext::EXPORT_ASSIGNMENT {
1085                return false;
1086            }
1087            // Stop at source file
1088            if node.kind == syntax_kind_ext::SOURCE_FILE {
1089                break;
1090            }
1091            // Walk to parent
1092            let Some(ext) = self.ctx.arena.get_extended(current) else {
1093                break;
1094            };
1095            if ext.parent.is_none() {
1096                break;
1097            }
1098            current = ext.parent;
1099        }
1100
1101        if could_be_in_initializer && !found_decl_in_path {
1102            // It was >= pos, but wasn't actually inside the declaration's AST.
1103            // This means it's strictly AFTER the declaration.
1104            return false;
1105        }
1106
1107        true
1108    }
1109
1110    /// Check if a modifier list in a specific arena contains the `declare` keyword.
1111    /// Used in multi-file mode where `self.ctx.arena` may not be the declaration's arena.
1112    pub(crate) fn has_declare_modifier_in_arena(
1113        &self,
1114        arena: &tsz_parser::parser::NodeArena,
1115        modifiers: &Option<tsz_parser::parser::NodeList>,
1116    ) -> bool {
1117        arena.has_modifier(modifiers, tsz_scanner::SyntaxKind::DeclareKeyword)
1118    }
1119
1120    /// Check if a node is in a type-only context (type annotation, type query, heritage clause).
1121    /// References in type-only positions don't need TDZ checks because types are
1122    /// resolved at compile-time, not runtime.
1123    fn is_in_type_only_context(&self, idx: NodeIndex) -> bool {
1124        use tsz_parser::parser::syntax_kind_ext;
1125
1126        let mut current = idx;
1127        while current.is_some() {
1128            let Some(ext) = self.ctx.arena.get_extended(current) else {
1129                return false;
1130            };
1131            if ext.parent.is_none() {
1132                return false;
1133            }
1134            let Some(parent_node) = self.ctx.arena.get(ext.parent) else {
1135                return false;
1136            };
1137
1138            // Type node kinds indicate we're in a type-only context
1139            match parent_node.kind {
1140                // Core type nodes
1141                syntax_kind_ext::TYPE_PREDICATE
1142                | syntax_kind_ext::TYPE_REFERENCE
1143                | syntax_kind_ext::FUNCTION_TYPE
1144                | syntax_kind_ext::CONSTRUCTOR_TYPE
1145                | syntax_kind_ext::TYPE_QUERY // typeof T in type position
1146                | syntax_kind_ext::TYPE_LITERAL
1147                | syntax_kind_ext::ARRAY_TYPE
1148                | syntax_kind_ext::TUPLE_TYPE
1149                | syntax_kind_ext::OPTIONAL_TYPE
1150                | syntax_kind_ext::REST_TYPE
1151                | syntax_kind_ext::UNION_TYPE
1152                | syntax_kind_ext::INTERSECTION_TYPE
1153                | syntax_kind_ext::CONDITIONAL_TYPE
1154                | syntax_kind_ext::INFER_TYPE
1155                | syntax_kind_ext::PARENTHESIZED_TYPE
1156                | syntax_kind_ext::THIS_TYPE
1157                | syntax_kind_ext::TYPE_OPERATOR
1158                | syntax_kind_ext::INDEXED_ACCESS_TYPE
1159                | syntax_kind_ext::MAPPED_TYPE
1160                | syntax_kind_ext::LITERAL_TYPE
1161                | syntax_kind_ext::NAMED_TUPLE_MEMBER
1162                | syntax_kind_ext::TEMPLATE_LITERAL_TYPE
1163                | syntax_kind_ext::IMPORT_TYPE
1164                | syntax_kind_ext::HERITAGE_CLAUSE
1165                | syntax_kind_ext::EXPRESSION_WITH_TYPE_ARGUMENTS => return true,
1166
1167                // Stop at boundaries that separate type from value context
1168                syntax_kind_ext::TYPE_OF_EXPRESSION // typeof x in value position
1169                | syntax_kind_ext::SOURCE_FILE => return false,
1170
1171                _ => {
1172                    // Continue walking up
1173                    current = ext.parent;
1174                }
1175            }
1176        }
1177        false
1178    }
1179
1180    /// Find the enclosing function-like node or source file for a given node.
1181    fn find_enclosing_function_or_source_file(&self, idx: NodeIndex) -> NodeIndex {
1182        use tsz_parser::parser::syntax_kind_ext;
1183
1184        let mut current = idx;
1185        while current.is_some() {
1186            let Some(node) = self.ctx.arena.get(current) else {
1187                break;
1188            };
1189            if node.is_function_like() || node.kind == syntax_kind_ext::SOURCE_FILE {
1190                return current;
1191            }
1192            let Some(ext) = self.ctx.arena.get_extended(current) else {
1193                break;
1194            };
1195            if ext.parent.is_none() {
1196                break;
1197            }
1198            current = ext.parent;
1199        }
1200        current
1201    }
1202}
1203
1204const fn is_unconditional_top_level_statement(kind: u16) -> bool {
1205    kind == syntax_kind_ext::VARIABLE_STATEMENT || kind == syntax_kind_ext::FOR_STATEMENT
1206}
1207
1208fn should_skip_daa_for_initialized_function_scoped_var(
1209    is_function_scoped: bool,
1210    is_source_file_global: bool,
1211    top_level_statement_kind: Option<u16>,
1212    usage_pos: u32,
1213    declaration_end: u32,
1214) -> bool {
1215    is_function_scoped
1216        && is_source_file_global
1217        && usage_pos >= declaration_end
1218        && top_level_statement_kind.is_some_and(is_unconditional_top_level_statement)
1219}
1220
1221#[cfg(test)]
1222mod tests {
1223    use super::{should_skip_daa_for_initialized_function_scoped_var, syntax_kind_ext};
1224
1225    #[test]
1226    fn skips_after_top_level_var_initializer_runs() {
1227        assert!(should_skip_daa_for_initialized_function_scoped_var(
1228            true,
1229            true,
1230            Some(syntax_kind_ext::VARIABLE_STATEMENT),
1231            100,
1232            50
1233        ));
1234    }
1235
1236    #[test]
1237    fn skips_after_top_level_for_initializer_runs() {
1238        assert!(should_skip_daa_for_initialized_function_scoped_var(
1239            true,
1240            true,
1241            Some(syntax_kind_ext::FOR_STATEMENT),
1242            200,
1243            80
1244        ));
1245    }
1246
1247    #[test]
1248    fn does_not_skip_when_declaration_is_conditional() {
1249        assert!(!should_skip_daa_for_initialized_function_scoped_var(
1250            true,
1251            true,
1252            Some(syntax_kind_ext::IF_STATEMENT),
1253            120,
1254            40
1255        ));
1256    }
1257
1258    #[test]
1259    fn does_not_skip_when_usage_precedes_declaration_end() {
1260        assert!(!should_skip_daa_for_initialized_function_scoped_var(
1261            true,
1262            true,
1263            Some(syntax_kind_ext::VARIABLE_STATEMENT),
1264            30,
1265            40
1266        ));
1267    }
1268}