Skip to main content

tsz_checker/symbols/
symbol_resolver.rs

1//! Symbol resolution helpers (type resolution, intrinsic detection, identifier lookup).
2//! - Qualified name resolution
3//! - Private identifier resolution
4//! - Type parameter resolution
5//! - Library type resolution
6//! - Type query resolution
7//! - Namespace member resolution
8//! - Global value resolution
9//! - Heritage symbol resolution
10//! - Access class resolution
11//!
12//! This module extends `CheckerState` with additional methods for symbol-related
13//! operations, providing cleaner APIs for common patterns.
14
15use crate::state::CheckerState;
16use std::sync::Arc;
17use tracing::trace;
18use tsz_binder::{SymbolId, symbol_flags};
19use tsz_parser::parser::NodeIndex;
20use tsz_parser::parser::syntax_kind_ext;
21use tsz_scanner::SyntaxKind;
22use tsz_solver::TypeId;
23use tsz_solver::is_compiler_managed_type;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum TypeSymbolResolution {
27    Type(SymbolId),
28    ValueOnly(SymbolId),
29    NotFound,
30}
31
32// =============================================================================
33// Symbol Resolution Methods
34// =============================================================================
35
36impl<'a> CheckerState<'a> {
37    // =========================================================================
38    // Symbol Type Resolution
39    // =========================================================================
40
41    /// Get the type of a symbol with caching.
42    ///
43    /// This is a convenience wrapper around `get_type_of_symbol` that
44    /// provides a clearer name for the operation.
45    pub fn get_symbol_type(&mut self, sym_id: SymbolId) -> TypeId {
46        self.get_type_of_symbol(sym_id)
47    }
48
49    // =========================================================================
50    // Global Symbol Detection
51    // =========================================================================
52
53    /// Check if a name refers to a global intrinsic value.
54    ///
55    /// Returns true for names like `undefined`, `NaN`, `Infinity`, etc.
56    pub fn is_global_intrinsic(&self, name: &str) -> bool {
57        matches!(
58            name,
59            "undefined"
60                | "NaN"
61                | "Infinity"
62                | "Math"
63                | "JSON"
64                | "Object"
65                | "Array"
66                | "String"
67                | "Number"
68                | "Boolean"
69                | "Symbol"
70                | "Date"
71                | "RegExp"
72                | "Error"
73                | "Function"
74                | "Promise"
75        )
76    }
77
78    /// Check if a name refers to a global constructor.
79    ///
80    /// Returns true for built-in constructor names like `Object`, `Array`, etc.
81    pub fn is_global_constructor(&self, name: &str) -> bool {
82        matches!(
83            name,
84            "Object"
85                | "Array"
86                | "String"
87                | "Number"
88                | "Boolean"
89                | "Symbol"
90                | "Date"
91                | "RegExp"
92                | "Error"
93                | "Function"
94                | "Promise"
95                | "Map"
96                | "Set"
97                | "WeakMap"
98                | "WeakSet"
99                | "Proxy"
100                | "Reflect"
101        )
102    }
103
104    // =========================================================================
105    // Symbol Information Queries
106    // =========================================================================
107
108    /// Get the name of a symbol.
109    ///
110    /// Returns the symbol's name as a string, or None if the symbol doesn't exist.
111    pub fn get_symbol_name(&self, sym_id: SymbolId) -> Option<String> {
112        self.ctx
113            .binder
114            .symbols
115            .get(sym_id)
116            .map(|symbol| symbol.escaped_name.clone())
117    }
118
119    /// Check if a symbol is exported.
120    ///
121    /// Returns true if the symbol has the exported flag set.
122    pub fn is_symbol_exported(&self, sym_id: SymbolId) -> bool {
123        self.ctx
124            .binder
125            .symbols
126            .get(sym_id)
127            .is_some_and(|symbol| symbol.is_exported)
128    }
129
130    /// Check if a symbol is type-only (e.g., from `import type`).
131    ///
132    /// Returns true if the symbol has the type-only flag set.
133    pub fn is_symbol_type_only(&self, sym_id: SymbolId) -> bool {
134        self.ctx
135            .binder
136            .symbols
137            .get(sym_id)
138            .is_some_and(|symbol| symbol.is_type_only)
139    }
140
141    // =========================================================================
142    // Symbol Property Queries
143    // =========================================================================
144
145    /// Get the value declaration of a symbol.
146    ///
147    /// Returns the primary value declaration node for the symbol, if any.
148    pub fn get_symbol_value_declaration(
149        &self,
150        sym_id: SymbolId,
151    ) -> Option<tsz_parser::parser::NodeIndex> {
152        self.ctx.binder.symbols.get(sym_id).and_then(|symbol| {
153            let decl = symbol.value_declaration;
154            (decl.0 != u32::MAX).then_some(decl)
155        })
156    }
157
158    /// Get all declarations for a symbol.
159    ///
160    /// Returns all declaration nodes associated with the symbol.
161    pub fn get_symbol_declarations(&self, sym_id: SymbolId) -> Vec<tsz_parser::parser::NodeIndex> {
162        self.ctx
163            .binder
164            .symbols
165            .get(sym_id)
166            .map(|symbol| symbol.declarations.clone())
167            .unwrap_or_default()
168    }
169
170    /// Check if a symbol has a specific flag.
171    ///
172    /// Returns true if the symbol has the specified flag bit set.
173    /// Returns false if the symbol doesn't exist.
174    pub fn symbol_has_flag(&self, sym_id: SymbolId, flag: u32) -> bool {
175        self.ctx
176            .binder
177            .symbols
178            .get(sym_id)
179            .is_some_and(|symbol| (symbol.flags & flag) != 0)
180    }
181
182    /// Safely get symbol flags, returning 0 if symbol doesn't exist.
183    ///
184    /// This defensive accessor prevents crashes when symbol IDs are invalid
185    /// or reference symbols that don't exist in any binder.
186    pub fn symbol_flags_safe(&self, sym_id: SymbolId) -> u32 {
187        self.ctx
188            .binder
189            .symbols
190            .get(sym_id)
191            .map_or(0, |symbol| symbol.flags)
192    }
193
194    /// Safely get symbol flags with lib binders fallback.
195    ///
196    /// Returns 0 if the symbol doesn't exist in any binder.
197    pub fn symbol_flags_with_libs(
198        &self,
199        sym_id: SymbolId,
200        lib_binders: &[Arc<tsz_binder::BinderState>],
201    ) -> u32 {
202        self.ctx
203            .binder
204            .get_symbol_with_libs(sym_id, lib_binders)
205            .map_or(0, |symbol| symbol.flags)
206    }
207
208    // =========================================================================
209    // Identifier Resolution
210    // =========================================================================
211
212    /// Collect lib binders from `lib_contexts` for cross-arena symbol lookup.
213    /// This enables symbol resolution across lib.d.ts files when `lib_binders`
214    /// is not populated in the binder (e.g., in the driver.rs path).
215    pub(crate) fn get_lib_binders(&self) -> Vec<Arc<tsz_binder::BinderState>> {
216        self.ctx
217            .lib_contexts
218            .iter()
219            .map(|lc| Arc::clone(&lc.binder))
220            .collect()
221    }
222
223    /// Check if a symbol represents a class member (property, method, accessor, or constructor).
224    ///
225    /// This filters out instance members that cannot be accessed as standalone values.
226    /// However, static members and constructors should still be accessible.
227    pub(crate) const fn is_class_member_symbol(flags: u32) -> bool {
228        // Check if it's any kind of class member
229        let is_member = (flags
230            & (symbol_flags::PROPERTY
231                | symbol_flags::METHOD
232                | symbol_flags::GET_ACCESSOR
233                | symbol_flags::SET_ACCESSOR
234                | symbol_flags::CONSTRUCTOR))
235            != 0;
236
237        if !is_member {
238            return false;
239        }
240
241        // Allow constructors - they represent the class itself
242        if (flags & symbol_flags::CONSTRUCTOR) != 0 {
243            return false;
244        }
245
246        // Allow static members - they're accessible via the class name
247        if (flags & symbol_flags::STATIC) != 0 {
248            return false;
249        }
250
251        // Filter out instance members (properties, methods, accessors without STATIC)
252        true
253    }
254
255    /// Resolve an identifier node to its symbol ID.
256    ///
257    /// This function walks the scope chain from the identifier's location upward,
258    /// checking each scope's symbol table for the name. It also checks:
259    /// - Module exports
260    /// - Type parameter scope (for generic functions, classes, type aliases)
261    /// - File locals (global scope from lib.d.ts)
262    /// - Lib binders' `file_locals`
263    ///
264    /// Returns None if the identifier cannot be resolved to any symbol.
265    pub(crate) fn resolve_identifier_symbol(&self, idx: NodeIndex) -> Option<SymbolId> {
266        let result = self.resolve_identifier_symbol_inner(idx);
267        if let Some(sym_id) = result {
268            self.ctx.referenced_symbols.borrow_mut().insert(sym_id);
269            trace!(sym_id = %sym_id.0, idx = %idx.0, "resolve_identifier_symbol: marked referenced");
270        }
271        result
272    }
273
274    /// Resolve identifier for write context (assignment target).
275    pub(crate) fn resolve_identifier_symbol_for_write(&self, idx: NodeIndex) -> Option<SymbolId> {
276        let result = self.resolve_identifier_symbol_inner(idx);
277        if let Some(sym_id) = result {
278            self.ctx.written_symbols.borrow_mut().insert(sym_id);
279        }
280        result
281    }
282
283    fn resolve_identifier_symbol_inner(&self, idx: NodeIndex) -> Option<SymbolId> {
284        // Get identifier name for tracing
285        let ident_name = self
286            .ctx
287            .arena
288            .get_identifier_at(idx)
289            .map(|i| i.escaped_text.as_str().to_string());
290
291        let ignore_libs = !self.ctx.has_lib_loaded();
292        let lib_binders = if ignore_libs {
293            Vec::new()
294        } else {
295            self.get_lib_binders()
296        };
297        let is_from_lib = |sym_id: SymbolId| self.ctx.symbol_is_from_lib(sym_id);
298        let should_skip_lib_symbol = |sym_id: SymbolId| ignore_libs && is_from_lib(sym_id);
299
300        trace!(
301            ident_name = ?ident_name,
302            idx = ?idx,
303            ignore_libs = ignore_libs,
304            "Resolving identifier symbol"
305        );
306
307        // First try the binder's resolver which checks scope chain and file_locals
308        let result = self.ctx.binder.resolve_identifier_with_filter(
309            self.ctx.arena,
310            idx,
311            &lib_binders,
312            |sym_id| {
313                if should_skip_lib_symbol(sym_id) {
314                    return false;
315                }
316                if let Some(symbol) = self.ctx.binder.get_symbol_with_libs(sym_id, &lib_binders) {
317                    let is_class_member = Self::is_class_member_symbol(symbol.flags);
318                    if is_class_member {
319                        return is_from_lib(sym_id)
320                            && (symbol.flags & symbol_flags::EXPORT_VALUE) != 0;
321                    }
322                }
323                true
324            },
325        );
326        let result = {
327            let expected_name = self
328                .ctx
329                .arena
330                .get_identifier_at(idx)
331                .map(|ident| ident.escaped_text.as_str());
332            result.filter(|&sym_id| {
333                let Some(expected_name) = expected_name else {
334                    return false;
335                };
336
337                self.ctx
338                    .binder
339                    .get_symbol_with_libs(sym_id, &lib_binders)
340                    .is_some_and(|symbol| symbol.escaped_name.as_str() == expected_name)
341            })
342        };
343
344        trace!(
345            ident_name = ?ident_name,
346            binder_result = ?result,
347            "Binder resolution result"
348        );
349
350        // IMPORTANT: If the binder didn't find the symbol, check lib_contexts directly as a fallback.
351        // The binder's method has a bug where it only queries lib_binders when lib_symbols_merged is FALSE.
352        // After lib symbols are merged into the main binder, lib_symbols_merged is set to TRUE,
353        // causing the binder to skip lib lookup entirely. By checking lib_contexts.file_locals
354        // directly here as a fallback, we bypass that bug and ensure global symbols are always resolved.
355        // This matches the pattern used successfully in generators.rs (lookup_global_type).
356        if result.is_none() && !ignore_libs {
357            // Get the identifier name
358            let name = if let Some(ident) = self.ctx.arena.get_identifier_at(idx) {
359                ident.escaped_text.as_str()
360            } else {
361                return None;
362            };
363            // Check lib_contexts directly for global symbols
364            for (lib_idx, lib_ctx) in self.ctx.lib_contexts.iter().enumerate() {
365                if let Some(lib_sym_id) = lib_ctx.binder.file_locals.get(name) {
366                    trace!(
367                        name = name,
368                        lib_idx = lib_idx,
369                        lib_sym_id = ?lib_sym_id,
370                        "Found symbol in lib_context"
371                    );
372                    if !should_skip_lib_symbol(lib_sym_id) {
373                        // Use file binder's sym_id for correct ID space after lib merge.
374                        // Never return lib-context SymbolIds directly: they may collide with
375                        // unrelated symbols in the current binder ID space.
376                        let Some(file_sym_id) = self.ctx.binder.file_locals.get(name) else {
377                            continue;
378                        };
379                        trace!(
380                            name = name,
381                            file_sym_id = ?file_sym_id,
382                            lib_sym_id = ?lib_sym_id,
383                            "Returning symbol from lib_contexts fallback"
384                        );
385                        return Some(file_sym_id);
386                    }
387                }
388            }
389        }
390
391        trace!(
392            ident_name = ?ident_name,
393            final_result = ?result,
394            "Symbol resolution final result"
395        );
396
397        if let Some(ident) = self.ctx.arena.get_identifier_at(idx)
398            && result.is_none()
399        {
400            let name = ident.escaped_text.as_str();
401            if let Some(sym_id) =
402                self.resolve_identifier_symbol_from_all_binders(name, |sym_id, symbol| {
403                    if should_skip_lib_symbol(sym_id) {
404                        return false;
405                    }
406
407                    let is_class_member = Self::is_class_member_symbol(symbol.flags);
408                    if is_class_member {
409                        return is_from_lib(sym_id)
410                            && (symbol.flags & symbol_flags::EXPORT_VALUE) != 0;
411                    }
412                    true
413                })
414            {
415                return Some(sym_id);
416            }
417        }
418
419        trace!(
420            ident_name = ?ident_name,
421            final_result = ?result,
422            "Symbol resolution final result"
423        );
424
425        if let Some(sym_id) = result
426            && let Some(sym) = self.ctx.binder.get_symbol_with_libs(sym_id, &lib_binders)
427        {
428            trace!(
429                ident_name = ?ident_name,
430                sym_id = sym_id.0,
431                sym_name = sym.escaped_name.as_str(),
432                sym_flags = sym.flags,
433                "Symbol resolution resolved metadata"
434            );
435        }
436        result
437    }
438
439    /// Resolve an identifier symbol for type positions, skipping value-only symbols.
440    pub(crate) fn resolve_identifier_symbol_in_type_position(
441        &self,
442        idx: NodeIndex,
443    ) -> TypeSymbolResolution {
444        let result = self.resolve_identifier_symbol_in_type_position_inner(idx);
445        if let TypeSymbolResolution::Type(sym_id) = result {
446            self.ctx.referenced_symbols.borrow_mut().insert(sym_id);
447        }
448        result
449    }
450
451    fn resolve_identifier_symbol_in_type_position_inner(
452        &self,
453        idx: NodeIndex,
454    ) -> TypeSymbolResolution {
455        let node = match self.ctx.arena.get(idx) {
456            Some(node) => node,
457            None => return TypeSymbolResolution::NotFound,
458        };
459        let ident = match self.ctx.arena.get_identifier(node) {
460            Some(ident) => ident,
461            None => return TypeSymbolResolution::NotFound,
462        };
463        let name = ident.escaped_text.as_str();
464
465        let ignore_libs = !self.ctx.has_lib_loaded();
466        // Collect lib binders for cross-arena symbol lookup
467        let lib_binders = if ignore_libs {
468            Vec::new()
469        } else {
470            self.get_lib_binders()
471        };
472        let should_skip_lib_symbol =
473            |sym_id: SymbolId| ignore_libs && self.ctx.symbol_is_from_lib(sym_id);
474        let mut value_only_candidate = None;
475
476        // Check if this name exists in a local scope (namespace/module) that would shadow
477        // the global lib symbol. If so, we skip the early lib_contexts check and let the
478        // binder's scope-based resolution find the local symbol first.
479        let name_in_local_scope = if !ignore_libs {
480            self.ctx
481                .binder
482                .resolve_identifier_with_filter(
483                    self.ctx.arena,
484                    idx,
485                    &lib_binders,
486                    |_| true, // accept any symbol
487                )
488                .is_some_and(|found_sym_id| {
489                    // Check if this symbol is different from the file_locals symbol.
490                    // If it's different, it was found in a more local scope (namespace, etc.)
491                    self.ctx.binder.file_locals.get(name) != Some(found_sym_id)
492                })
493        } else {
494            false
495        };
496
497        // IMPORTANT: Check lib_contexts directly BEFORE calling binder's resolve_identifier_with_filter.
498        // The binder's method has a bug where it only queries lib_binders when lib_symbols_merged is FALSE.
499        // After lib symbols are merged into the main binder, lib_symbols_merged is set to TRUE,
500        // causing the binder to skip lib lookup entirely. By checking lib_contexts.file_locals
501        // directly here, we bypass that bug and ensure global type symbols are always resolved.
502        // However, skip this early check when the name is declared in a local scope (namespace, etc.)
503        // so that local symbols can shadow global ones.
504        if !ignore_libs && !name_in_local_scope {
505            for lib_ctx in &self.ctx.lib_contexts {
506                if let Some(lib_sym_id) = lib_ctx.binder.file_locals.get(name) {
507                    // After lib merge, the file binder has the same symbols with
508                    // potentially different IDs. Use file binder's ID for returns,
509                    // and skip symbols not present in current binder ID space.
510                    let Some(sym_id) = self.ctx.binder.file_locals.get(name) else {
511                        continue;
512                    };
513                    if !should_skip_lib_symbol(sym_id) {
514                        // Check flags using lib binder (lib_sym_id is valid in lib binder)
515                        let flags = lib_ctx.binder.get_symbol(lib_sym_id).map_or(0, |s| s.flags);
516
517                        // Namespaces and modules are value-only but should be allowed in type position
518                        let is_namespace_or_module = (flags
519                            & (symbol_flags::NAMESPACE_MODULE | symbol_flags::VALUE_MODULE))
520                            != 0;
521
522                        if is_namespace_or_module {
523                            return TypeSymbolResolution::Type(sym_id);
524                        }
525
526                        // For ALIAS symbols, resolve to the target
527                        if flags & symbol_flags::ALIAS != 0 {
528                            let mut visited = Vec::new();
529                            if let Some(target_sym_id) =
530                                self.resolve_alias_symbol(sym_id, &mut visited)
531                            {
532                                // Check the target symbol's flags
533                                let target_flags = self
534                                    .ctx
535                                    .binder
536                                    .get_symbol_with_libs(target_sym_id, &lib_binders)
537                                    .map_or(0, |s| s.flags);
538                                if (target_flags
539                                    & (symbol_flags::NAMESPACE_MODULE | symbol_flags::VALUE_MODULE))
540                                    != 0
541                                {
542                                    return TypeSymbolResolution::Type(target_sym_id);
543                                }
544                            }
545                        }
546
547                        // Check if this is a value-only symbol
548                        let is_value_only = (self.alias_resolves_to_value_only(sym_id, None)
549                            || self.symbol_is_value_only(sym_id, None))
550                            && !self.symbol_is_type_only(sym_id, None);
551                        if is_value_only {
552                            if value_only_candidate.is_none() {
553                                value_only_candidate = Some(sym_id);
554                            }
555                        } else {
556                            // Valid type symbol found in lib
557                            return TypeSymbolResolution::Type(sym_id);
558                        }
559                    }
560                }
561            }
562        }
563
564        let mut accept_type_symbol = |sym_id: SymbolId| -> bool {
565            // Get symbol flags to check for special cases
566            let flags = self
567                .ctx
568                .binder
569                .get_symbol_with_libs(sym_id, &lib_binders)
570                .map_or(0, |s| s.flags);
571
572            // Namespaces and modules are value-only but should be allowed in type position
573            // because they can contain types (e.g., MyNamespace.ValueInterface)
574            let is_namespace_or_module =
575                (flags & (symbol_flags::NAMESPACE_MODULE | symbol_flags::VALUE_MODULE)) != 0;
576
577            if is_namespace_or_module {
578                return true;
579            }
580
581            // For ALIAS symbols (import equals declarations), resolve to the target
582            // and check if it's a namespace/module
583            if flags & symbol_flags::ALIAS != 0 {
584                let mut visited = Vec::new();
585                if let Some(target_sym_id) = self.resolve_alias_symbol(sym_id, &mut visited) {
586                    let target_flags = self
587                        .ctx
588                        .binder
589                        .get_symbol_with_libs(target_sym_id, &lib_binders)
590                        .map_or(0, |s| s.flags);
591                    if (target_flags
592                        & (symbol_flags::NAMESPACE_MODULE | symbol_flags::VALUE_MODULE))
593                        != 0
594                    {
595                        return true;
596                    }
597                }
598            }
599
600            let is_value_only = (self.alias_resolves_to_value_only(sym_id, None)
601                || self.symbol_is_value_only(sym_id, None))
602                && !self.symbol_is_type_only(sym_id, None);
603            if is_value_only {
604                if value_only_candidate.is_none() {
605                    value_only_candidate = Some(sym_id);
606                }
607                return false;
608            }
609            true
610        };
611
612        let resolved = self.ctx.binder.resolve_identifier_with_filter(
613            self.ctx.arena,
614            idx,
615            &lib_binders,
616            |sym_id| {
617                if should_skip_lib_symbol(sym_id) {
618                    return false;
619                }
620                if let Some(symbol) = self.ctx.binder.get_symbol_with_libs(sym_id, &lib_binders) {
621                    let is_class_member = Self::is_class_member_symbol(symbol.flags);
622                    if is_class_member {
623                        return false;
624                    }
625                }
626                accept_type_symbol(sym_id)
627            },
628        );
629
630        if resolved.is_none()
631            && let Some(sym_id) =
632                self.resolve_identifier_symbol_from_all_binders(name, |sym_id, symbol| {
633                    if should_skip_lib_symbol(sym_id) {
634                        return false;
635                    }
636
637                    let is_class_member = Self::is_class_member_symbol(symbol.flags);
638                    if is_class_member {
639                        return false;
640                    }
641                    accept_type_symbol(sym_id)
642                })
643        {
644            let is_value_only = (self.alias_resolves_to_value_only(sym_id, None)
645                || self.symbol_is_value_only(sym_id, None))
646                && !self.symbol_is_type_only(sym_id, None);
647            if is_value_only {
648                return TypeSymbolResolution::ValueOnly(sym_id);
649            }
650            return TypeSymbolResolution::Type(sym_id);
651        }
652
653        // Guard against SymbolId renumbering from lib merging: if the resolved
654        // symbol's name doesn't match the requested name, the scope table has a
655        // stale SymbolId. Reject it and fall through to value_only_candidate.
656        let resolved = resolved.filter(|&sym_id| {
657            self.ctx
658                .binder
659                .get_symbol_with_libs(sym_id, &lib_binders)
660                .is_some_and(|s| s.escaped_name.as_str() == name)
661        });
662        if let Some(sym_id) = resolved {
663            if let Some(symbol) = self.ctx.binder.get_symbol_with_libs(sym_id, &lib_binders)
664                && symbol.flags & symbol_flags::ALIAS != 0
665            {
666                // Mark the local alias as referenced (for unused-import tracking).
667                // When we follow the alias chain below, only the target gets returned
668                // and inserted into referenced_symbols by the caller. Without this,
669                // imports used only in type positions appear unused (false TS6133).
670                self.ctx.referenced_symbols.borrow_mut().insert(sym_id);
671                let mut visited_aliases = Vec::new();
672                if let Some(target_sym_id) = self.resolve_alias_symbol(sym_id, &mut visited_aliases)
673                {
674                    let target_flags = self
675                        .ctx
676                        .binder
677                        .get_symbol_with_libs(target_sym_id, &lib_binders)
678                        .map_or(0, |s| s.flags);
679                    let target_is_namespace_module = (target_flags
680                        & (symbol_flags::NAMESPACE_MODULE | symbol_flags::VALUE_MODULE))
681                        != 0;
682                    let target_is_value_only = (self
683                        .alias_resolves_to_value_only(target_sym_id, None)
684                        || self.symbol_is_value_only(target_sym_id, None))
685                        && !self.symbol_is_type_only(target_sym_id, None);
686                    if target_is_value_only && !target_is_namespace_module {
687                        return TypeSymbolResolution::ValueOnly(target_sym_id);
688                    }
689                    return TypeSymbolResolution::Type(target_sym_id);
690                }
691            }
692            return TypeSymbolResolution::Type(sym_id);
693        }
694
695        if let Some(value_only) = value_only_candidate {
696            TypeSymbolResolution::ValueOnly(value_only)
697        } else {
698            TypeSymbolResolution::NotFound
699        }
700    }
701
702    /// Resolve a private identifier to its symbols across class scopes.
703    ///
704    /// Private identifiers (e.g., `#foo`) are only valid within class bodies.
705    /// This function walks the scope chain and collects all symbols with the
706    /// matching private name from class scopes.
707    ///
708    /// Returns a tuple of (`symbols_found`, `saw_class_scope`) where:
709    /// - `symbols_found`: Vec of `SymbolIds` for all matching private members
710    /// - `saw_class_scope`: true if any class scope was encountered
711    pub(crate) fn resolve_private_identifier_symbols(
712        &self,
713        idx: NodeIndex,
714    ) -> (Vec<SymbolId>, bool) {
715        self.ctx
716            .binder
717            .resolve_private_identifier_symbols(self.ctx.arena, idx)
718    }
719
720    /// Resolve a qualified name or identifier to a symbol ID.
721    ///
722    /// Handles both simple identifiers and qualified names (e.g., `A.B.C`).
723    /// Also resolves through alias symbols (imports).
724    pub(crate) fn resolve_qualified_symbol(&self, idx: NodeIndex) -> Option<SymbolId> {
725        let mut visited_aliases = Vec::new();
726        self.resolve_qualified_symbol_inner(idx, &mut visited_aliases, 0)
727    }
728
729    /// Resolve a qualified name or identifier for type positions.
730    pub(crate) fn resolve_qualified_symbol_in_type_position(
731        &self,
732        idx: NodeIndex,
733    ) -> TypeSymbolResolution {
734        let mut visited_aliases = Vec::new();
735        self.resolve_qualified_symbol_inner_in_type_position(idx, &mut visited_aliases, 0)
736    }
737
738    /// Inner implementation of qualified symbol resolution for type positions.
739    pub(crate) fn resolve_qualified_symbol_inner_in_type_position(
740        &self,
741        idx: NodeIndex,
742        visited_aliases: &mut Vec<SymbolId>,
743        depth: usize,
744    ) -> TypeSymbolResolution {
745        // Prevent stack overflow from deeply nested qualified names
746        const MAX_QUALIFIED_NAME_DEPTH: usize = 128;
747        if depth >= MAX_QUALIFIED_NAME_DEPTH {
748            return TypeSymbolResolution::NotFound;
749        }
750
751        let node = match self.ctx.arena.get(idx) {
752            Some(node) => node,
753            None => return TypeSymbolResolution::NotFound,
754        };
755
756        if node.kind == SyntaxKind::Identifier as u16 {
757            return match self.resolve_identifier_symbol_in_type_position(idx) {
758                TypeSymbolResolution::Type(sym_id) => {
759                    // Preserve unresolved alias symbols in type position.
760                    // `import X = require("...")` aliases may not resolve to a concrete
761                    // target symbol, but `X` is still a valid namespace-like type query
762                    // anchor (e.g., `typeof X.Member`).
763                    let resolved = self
764                        .resolve_alias_symbol(sym_id, visited_aliases)
765                        .unwrap_or(sym_id);
766                    TypeSymbolResolution::Type(resolved)
767                }
768                other => other,
769            };
770        }
771
772        if node.kind == SyntaxKind::StringLiteral as u16
773            || node.kind == SyntaxKind::NoSubstitutionTemplateLiteral as u16
774        {
775            let Some(literal) = self.ctx.arena.get_literal(node) else {
776                return TypeSymbolResolution::NotFound;
777            };
778            if let Some(sym_id) = self.ctx.binder.file_locals.get(&literal.text) {
779                let is_value_only = (self
780                    .alias_resolves_to_value_only(sym_id, Some(&literal.text))
781                    || self.symbol_is_value_only(sym_id, Some(&literal.text)))
782                    && !self.symbol_is_type_only(sym_id, Some(&literal.text));
783                if is_value_only {
784                    return TypeSymbolResolution::ValueOnly(sym_id);
785                }
786                let Some(sym_id) = self.resolve_alias_symbol(sym_id, visited_aliases) else {
787                    return TypeSymbolResolution::NotFound;
788                };
789                return TypeSymbolResolution::Type(sym_id);
790            }
791            return TypeSymbolResolution::NotFound;
792        }
793
794        if node.kind == tsz_parser::parser::syntax_kind_ext::PROPERTY_ACCESS_EXPRESSION {
795            let Some(access) = self.ctx.arena.get_access_expr(node) else {
796                return TypeSymbolResolution::NotFound;
797            };
798
799            let left_sym = match self.resolve_qualified_symbol_inner_in_type_position(
800                access.expression,
801                visited_aliases,
802                depth + 1,
803            ) {
804                TypeSymbolResolution::Type(sym_id) => sym_id,
805                other => return other,
806            };
807
808            let left_sym = self
809                .resolve_alias_symbol(left_sym, visited_aliases)
810                .unwrap_or(left_sym);
811
812            let right_name = match self
813                .ctx
814                .arena
815                .get_identifier_at(access.name_or_argument)
816                .map(|ident| ident.escaped_text.as_str())
817            {
818                Some(name) => name,
819                None => return TypeSymbolResolution::NotFound,
820            };
821
822            let lib_binders = self.get_lib_binders();
823            let Some(left_symbol) = self.ctx.binder.get_symbol_with_libs(left_sym, &lib_binders)
824            else {
825                return TypeSymbolResolution::NotFound;
826            };
827
828            if let Some(exports) = left_symbol.exports.as_ref()
829                && let Some(member_sym) = exports.get(right_name)
830            {
831                let is_value_only = (self
832                    .alias_resolves_to_value_only(member_sym, Some(right_name))
833                    || self.symbol_is_value_only(member_sym, Some(right_name)))
834                    && !self.symbol_is_type_only(member_sym, Some(right_name));
835                if is_value_only {
836                    return TypeSymbolResolution::ValueOnly(member_sym);
837                }
838                let member_sym = self
839                    .resolve_alias_symbol(member_sym, visited_aliases)
840                    .unwrap_or(member_sym);
841                return TypeSymbolResolution::Type(member_sym);
842            }
843
844            if let Some(ref module_specifier) = left_symbol.import_module
845                && !((left_symbol.flags & symbol_flags::ALIAS) != 0
846                    && self
847                        .ctx
848                        .module_resolves_to_non_module_entity(module_specifier))
849                && let Some(reexported_sym) = self.resolve_reexported_member_symbol(
850                    module_specifier,
851                    right_name,
852                    visited_aliases,
853                )
854            {
855                let is_value_only = (self
856                    .alias_resolves_to_value_only(reexported_sym, Some(right_name))
857                    || self.symbol_is_value_only(reexported_sym, Some(right_name)))
858                    && !self.symbol_is_type_only(reexported_sym, Some(right_name));
859                if is_value_only {
860                    return TypeSymbolResolution::ValueOnly(reexported_sym);
861                }
862                return TypeSymbolResolution::Type(reexported_sym);
863            }
864
865            if let Some(reexported_sym) =
866                self.resolve_member_from_import_equals_alias(left_sym, right_name, visited_aliases)
867            {
868                let is_value_only = (self
869                    .alias_resolves_to_value_only(reexported_sym, Some(right_name))
870                    || self.symbol_is_value_only(reexported_sym, Some(right_name)))
871                    && !self.symbol_is_type_only(reexported_sym, Some(right_name));
872                if is_value_only {
873                    return TypeSymbolResolution::ValueOnly(reexported_sym);
874                }
875                return TypeSymbolResolution::Type(reexported_sym);
876            }
877
878            return TypeSymbolResolution::NotFound;
879        }
880
881        if node.kind != tsz_parser::parser::syntax_kind_ext::QUALIFIED_NAME {
882            return TypeSymbolResolution::NotFound;
883        }
884
885        let qn = match self.ctx.arena.get_qualified_name(node) {
886            Some(qn) => qn,
887            None => return TypeSymbolResolution::NotFound,
888        };
889        let left_sym = match self.resolve_qualified_symbol_inner_in_type_position(
890            qn.left,
891            visited_aliases,
892            depth + 1,
893        ) {
894            TypeSymbolResolution::Type(sym_id) => sym_id,
895            other => return other,
896        };
897        let left_sym = self
898            .resolve_alias_symbol(left_sym, visited_aliases)
899            .unwrap_or(left_sym);
900        let right_name = match self
901            .ctx
902            .arena
903            .get(qn.right)
904            .and_then(|node| self.ctx.arena.get_identifier(node))
905            .map(|ident| ident.escaped_text.as_str())
906        {
907            Some(name) => name,
908            None => return TypeSymbolResolution::NotFound,
909        };
910
911        // Look up the symbol across binders (file + libs)
912        let lib_binders = self.get_lib_binders();
913        let Some(left_symbol) = self.ctx.binder.get_symbol_with_libs(left_sym, &lib_binders) else {
914            return TypeSymbolResolution::NotFound;
915        };
916        // First try direct exports
917        if let Some(exports) = left_symbol.exports.as_ref()
918            && let Some(member_sym) = exports.get(right_name)
919        {
920            let is_value_only = (self.alias_resolves_to_value_only(member_sym, Some(right_name))
921                || self.symbol_is_value_only(member_sym, Some(right_name)))
922                && !self.symbol_is_type_only(member_sym, Some(right_name));
923            if is_value_only {
924                return TypeSymbolResolution::ValueOnly(member_sym);
925            }
926            return TypeSymbolResolution::Type(
927                self.resolve_alias_symbol(member_sym, visited_aliases)
928                    .unwrap_or(member_sym),
929            );
930        }
931
932        // If not found in direct exports, check for re-exports
933        if let Some(ref module_specifier) = left_symbol.import_module {
934            if (left_symbol.flags & symbol_flags::ALIAS) != 0
935                && self
936                    .ctx
937                    .module_resolves_to_non_module_entity(module_specifier)
938            {
939                return TypeSymbolResolution::NotFound;
940            }
941            if let Some(reexported_sym) =
942                self.resolve_reexported_member_symbol(module_specifier, right_name, visited_aliases)
943            {
944                let is_value_only = (self
945                    .alias_resolves_to_value_only(reexported_sym, Some(right_name))
946                    || self.symbol_is_value_only(reexported_sym, Some(right_name)))
947                    && !self.symbol_is_type_only(reexported_sym, Some(right_name));
948                if is_value_only {
949                    return TypeSymbolResolution::ValueOnly(reexported_sym);
950                }
951                return TypeSymbolResolution::Type(reexported_sym);
952            }
953        }
954
955        if let Some(reexported_sym) =
956            self.resolve_member_from_import_equals_alias(left_sym, right_name, visited_aliases)
957        {
958            let is_value_only = (self
959                .alias_resolves_to_value_only(reexported_sym, Some(right_name))
960                || self.symbol_is_value_only(reexported_sym, Some(right_name)))
961                && !self.symbol_is_type_only(reexported_sym, Some(right_name));
962            if is_value_only {
963                return TypeSymbolResolution::ValueOnly(reexported_sym);
964            }
965            return TypeSymbolResolution::Type(reexported_sym);
966        }
967
968        TypeSymbolResolution::NotFound
969    }
970
971    fn resolve_identifier_symbol_from_all_binders(
972        &self,
973        name: &str,
974        mut accept: impl FnMut(SymbolId, &tsz_binder::Symbol) -> bool,
975    ) -> Option<SymbolId> {
976        let all_binders = self.ctx.all_binders.as_ref()?;
977
978        for (file_idx, binder) in all_binders.iter().enumerate() {
979            if let Some(sym_id) = binder.file_locals.get(name) {
980                let Some(sym_symbol) = binder.get_symbol(sym_id) else {
981                    continue;
982                };
983                if !accept(sym_id, sym_symbol) {
984                    continue;
985                }
986                if let Some(local_symbol) = self.ctx.binder.get_symbol(sym_id) {
987                    if local_symbol.escaped_name != name {
988                        self.ctx
989                            .cross_file_symbol_targets
990                            .borrow_mut()
991                            .entry(sym_id)
992                            .or_insert(file_idx);
993                    }
994                } else {
995                    self.ctx
996                        .cross_file_symbol_targets
997                        .borrow_mut()
998                        .entry(sym_id)
999                        .or_insert(file_idx);
1000                }
1001                return Some(sym_id);
1002            }
1003        }
1004
1005        None
1006    }
1007
1008    /// Inner implementation of qualified symbol resolution with cycle detection.
1009    pub(crate) fn resolve_qualified_symbol_inner(
1010        &self,
1011        idx: NodeIndex,
1012        visited_aliases: &mut Vec<SymbolId>,
1013        depth: usize,
1014    ) -> Option<SymbolId> {
1015        // Prevent stack overflow from deeply nested qualified names
1016        const MAX_QUALIFIED_NAME_DEPTH: usize = 128;
1017        if depth >= MAX_QUALIFIED_NAME_DEPTH {
1018            return None;
1019        }
1020
1021        let node = self.ctx.arena.get(idx)?;
1022
1023        if node.kind == SyntaxKind::Identifier as u16 {
1024            let sym_id = self.resolve_identifier_symbol(idx)?;
1025            // Preserve alias symbols when alias resolution has no concrete target
1026            // (e.g., `import X = require("...")` namespace-like aliases).
1027            return self
1028                .resolve_alias_symbol(sym_id, visited_aliases)
1029                .or(Some(sym_id));
1030        }
1031
1032        if node.kind == SyntaxKind::StringLiteral as u16
1033            || node.kind == SyntaxKind::NoSubstitutionTemplateLiteral as u16
1034        {
1035            let literal = self.ctx.arena.get_literal(node)?;
1036            if let Some(sym_id) = self.ctx.binder.file_locals.get(&literal.text) {
1037                return self.resolve_alias_symbol(sym_id, visited_aliases);
1038            }
1039            return None;
1040        }
1041
1042        if node.kind == tsz_parser::parser::syntax_kind_ext::PROPERTY_ACCESS_EXPRESSION {
1043            let access = self.ctx.arena.get_access_expr(node)?;
1044            let left_sym =
1045                self.resolve_qualified_symbol_inner(access.expression, visited_aliases, depth + 1)?;
1046            let left_sym = self
1047                .resolve_alias_symbol(left_sym, visited_aliases)
1048                .unwrap_or(left_sym);
1049            let right_name = self
1050                .ctx
1051                .arena
1052                .get_identifier_at(access.name_or_argument)
1053                .map(|ident| ident.escaped_text.as_str())?;
1054
1055            let lib_binders = self.get_lib_binders();
1056            let left_symbol = self
1057                .ctx
1058                .binder
1059                .get_symbol_with_libs(left_sym, &lib_binders)?;
1060
1061            if let Some(exports) = left_symbol.exports.as_ref()
1062                && let Some(member_sym) = exports.get(right_name)
1063            {
1064                return Some(
1065                    self.resolve_alias_symbol(member_sym, visited_aliases)
1066                        .unwrap_or(member_sym),
1067                );
1068            }
1069
1070            if let Some(ref module_specifier) = left_symbol.import_module {
1071                if (left_symbol.flags & symbol_flags::ALIAS) != 0
1072                    && self
1073                        .ctx
1074                        .module_resolves_to_non_module_entity(module_specifier)
1075                {
1076                    return None;
1077                }
1078                return self.resolve_reexported_member_symbol(
1079                    module_specifier,
1080                    right_name,
1081                    visited_aliases,
1082                );
1083            }
1084
1085            if let Some(reexported_sym) =
1086                self.resolve_member_from_import_equals_alias(left_sym, right_name, visited_aliases)
1087            {
1088                return Some(reexported_sym);
1089            }
1090
1091            return None;
1092        }
1093
1094        if node.kind != tsz_parser::parser::syntax_kind_ext::QUALIFIED_NAME {
1095            return None;
1096        }
1097
1098        let qn = self.ctx.arena.get_qualified_name(node)?;
1099        let left_sym = self.resolve_qualified_symbol_inner(qn.left, visited_aliases, depth + 1)?;
1100        let left_sym = self
1101            .resolve_alias_symbol(left_sym, visited_aliases)
1102            .unwrap_or(left_sym);
1103        let right_name = self
1104            .ctx
1105            .arena
1106            .get(qn.right)
1107            .and_then(|node| self.ctx.arena.get_identifier(node))
1108            .map(|ident| ident.escaped_text.as_str())?;
1109
1110        let lib_binders = self.get_lib_binders();
1111        let left_symbol = self
1112            .ctx
1113            .binder
1114            .get_symbol_with_libs(left_sym, &lib_binders)?;
1115
1116        // First try direct exports
1117        if let Some(exports) = left_symbol.exports.as_ref()
1118            && let Some(member_sym) = exports.get(right_name)
1119        {
1120            return Some(
1121                self.resolve_alias_symbol(member_sym, visited_aliases)
1122                    .unwrap_or(member_sym),
1123            );
1124        }
1125
1126        // If not found in direct exports, check for re-exports
1127        // This handles cases like: export { foo } from './bar'
1128        if let Some(ref module_specifier) = left_symbol.import_module {
1129            if (left_symbol.flags & symbol_flags::ALIAS) != 0
1130                && self
1131                    .ctx
1132                    .module_resolves_to_non_module_entity(module_specifier)
1133            {
1134                return None;
1135            }
1136            if let Some(reexported_sym) =
1137                self.resolve_reexported_member_symbol(module_specifier, right_name, visited_aliases)
1138            {
1139                return Some(reexported_sym);
1140            }
1141        }
1142
1143        if let Some(reexported_sym) =
1144            self.resolve_member_from_import_equals_alias(left_sym, right_name, visited_aliases)
1145        {
1146            return Some(reexported_sym);
1147        }
1148
1149        None
1150    }
1151
1152    fn resolve_member_from_import_equals_alias(
1153        &self,
1154        alias_sym: SymbolId,
1155        member_name: &str,
1156        visited_aliases: &mut Vec<SymbolId>,
1157    ) -> Option<SymbolId> {
1158        let symbol = self.ctx.binder.get_symbol(alias_sym)?;
1159        if symbol.flags & symbol_flags::ALIAS == 0 {
1160            return None;
1161        }
1162
1163        let decl_idx = if symbol.value_declaration.is_some() {
1164            symbol.value_declaration
1165        } else {
1166            symbol
1167                .declarations
1168                .iter()
1169                .copied()
1170                .find(|idx| idx.is_some())
1171                .unwrap_or(NodeIndex::NONE)
1172        };
1173
1174        if decl_idx.is_some()
1175            && let Some(decl_node) = self.ctx.arena.get(decl_idx)
1176            && decl_node.kind == syntax_kind_ext::IMPORT_EQUALS_DECLARATION
1177            && let Some(import) = self.ctx.arena.get_import_decl(decl_node)
1178            && let Some(module_specifier) =
1179                self.get_require_module_specifier(import.module_specifier)
1180        {
1181            if self
1182                .ctx
1183                .module_resolves_to_non_module_entity(&module_specifier)
1184            {
1185                return None;
1186            }
1187            return self.resolve_reexported_member_symbol(
1188                &module_specifier,
1189                member_name,
1190                visited_aliases,
1191            );
1192        }
1193
1194        None
1195    }
1196
1197    /// Resolve a re-exported member symbol by following re-export chains.
1198    ///
1199    /// This function handles cases where a namespace member is re-exported from
1200    /// another module using `export { foo } from './bar'` or `export * from './bar'`.
1201    pub(crate) fn resolve_reexported_member_symbol(
1202        &self,
1203        module_specifier: &str,
1204        member_name: &str,
1205        visited_aliases: &mut Vec<SymbolId>,
1206    ) -> Option<SymbolId> {
1207        let mut visited_modules = rustc_hash::FxHashSet::default();
1208        self.resolve_reexported_member_symbol_inner(
1209            module_specifier,
1210            member_name,
1211            visited_aliases,
1212            &mut visited_modules,
1213        )
1214    }
1215
1216    fn resolve_member_from_module_exports(
1217        &self,
1218        binder: &tsz_binder::BinderState,
1219        exports_table: &tsz_binder::SymbolTable,
1220        member_name: &str,
1221        visited_aliases: &mut Vec<SymbolId>,
1222    ) -> Option<SymbolId> {
1223        let can_resolve_aliases = std::ptr::eq(binder, self.ctx.binder);
1224
1225        if let Some(sym_id) = exports_table.get(member_name) {
1226            if can_resolve_aliases {
1227                return Some(
1228                    self.resolve_alias_symbol(sym_id, visited_aliases)
1229                        .unwrap_or(sym_id),
1230                );
1231            }
1232            return Some(sym_id);
1233        }
1234
1235        let export_equals_sym = exports_table.get("export=")?;
1236        let mut candidate_symbol_ids = vec![export_equals_sym];
1237        if can_resolve_aliases {
1238            let resolved_export_equals = self
1239                .resolve_alias_symbol(export_equals_sym, visited_aliases)
1240                .unwrap_or(export_equals_sym);
1241            if resolved_export_equals != export_equals_sym {
1242                candidate_symbol_ids.push(resolved_export_equals);
1243            }
1244        }
1245
1246        for candidate_symbol_id in candidate_symbol_ids {
1247            let Some(target_symbol) = binder.get_symbol(candidate_symbol_id) else {
1248                continue;
1249            };
1250
1251            if let Some(exports) = target_symbol.exports.as_ref()
1252                && let Some(sym_id) = exports.get(member_name)
1253            {
1254                if can_resolve_aliases {
1255                    return Some(
1256                        self.resolve_alias_symbol(sym_id, visited_aliases)
1257                            .unwrap_or(sym_id),
1258                    );
1259                }
1260                return Some(sym_id);
1261            }
1262
1263            if let Some(members) = target_symbol.members.as_ref()
1264                && let Some(sym_id) = members.get(member_name)
1265            {
1266                if can_resolve_aliases {
1267                    return Some(
1268                        self.resolve_alias_symbol(sym_id, visited_aliases)
1269                            .unwrap_or(sym_id),
1270                    );
1271                }
1272                return Some(sym_id);
1273            }
1274
1275            // Some binder states keep the namespace merge partner as a distinct symbol.
1276            // Search same-name symbols with module namespace flags for members.
1277            for merged_candidate_id in binder
1278                .get_symbols()
1279                .find_all_by_name(&target_symbol.escaped_name)
1280            {
1281                let Some(merged_symbol) = binder.get_symbol(merged_candidate_id) else {
1282                    continue;
1283                };
1284                if (merged_symbol.flags
1285                    & (symbol_flags::MODULE
1286                        | symbol_flags::NAMESPACE_MODULE
1287                        | symbol_flags::VALUE_MODULE))
1288                    == 0
1289                {
1290                    continue;
1291                }
1292
1293                if let Some(exports) = merged_symbol.exports.as_ref()
1294                    && let Some(sym_id) = exports.get(member_name)
1295                {
1296                    if can_resolve_aliases {
1297                        return Some(
1298                            self.resolve_alias_symbol(sym_id, visited_aliases)
1299                                .unwrap_or(sym_id),
1300                        );
1301                    }
1302                    return Some(sym_id);
1303                }
1304
1305                if let Some(members) = merged_symbol.members.as_ref()
1306                    && let Some(sym_id) = members.get(member_name)
1307                {
1308                    if can_resolve_aliases {
1309                        return Some(
1310                            self.resolve_alias_symbol(sym_id, visited_aliases)
1311                                .unwrap_or(sym_id),
1312                        );
1313                    }
1314                    return Some(sym_id);
1315                }
1316            }
1317        }
1318
1319        None
1320    }
1321
1322    /// Inner implementation with cycle detection for module re-exports.
1323    fn resolve_reexported_member_symbol_inner(
1324        &self,
1325        module_specifier: &str,
1326        member_name: &str,
1327        visited_aliases: &mut Vec<SymbolId>,
1328        visited_modules: &mut rustc_hash::FxHashSet<(String, String)>,
1329    ) -> Option<SymbolId> {
1330        // Cycle detection: check if we've already visited this (module, member) pair
1331        let key = (module_specifier.to_string(), member_name.to_string());
1332        if visited_modules.contains(&key) {
1333            return None;
1334        }
1335        visited_modules.insert(key);
1336
1337        // First, check if it's a direct export from this module (ambient modules)
1338        if let Some(module_exports) = self.ctx.binder.module_exports.get(module_specifier)
1339            && let Some(sym_id) = self.resolve_member_from_module_exports(
1340                self.ctx.binder,
1341                module_exports,
1342                member_name,
1343                visited_aliases,
1344            )
1345        {
1346            return Some(sym_id);
1347        }
1348
1349        // Cross-file resolution: use canonical file-key lookups via state_type_resolution.
1350        if let Some(sym_id) = self.resolve_cross_file_export(module_specifier, member_name) {
1351            return Some(
1352                self.resolve_alias_symbol(sym_id, visited_aliases)
1353                    .unwrap_or(sym_id),
1354            );
1355        }
1356
1357        // Check for named re-exports: `export { foo } from 'bar'`
1358        if let Some(file_reexports) = self.ctx.binder.reexports.get(module_specifier)
1359            && let Some((source_module, original_name)) = file_reexports.get(member_name)
1360        {
1361            let name_to_lookup = original_name.as_deref().unwrap_or(member_name);
1362            return self.resolve_reexported_member_symbol_inner(
1363                source_module,
1364                name_to_lookup,
1365                visited_aliases,
1366                visited_modules,
1367            );
1368        }
1369
1370        // Check for wildcard re-exports: `export * from 'bar'`
1371        // TSC behavior: If two `export *` declarations export the same name,
1372        // that name is considered AMBIGUOUS and is NOT exported
1373        // (unless explicitly re-exported by name, which is checked above).
1374        if let Some(source_modules) = self.ctx.binder.wildcard_reexports.get(module_specifier) {
1375            let mut found_result: Option<SymbolId> = None;
1376            let mut found_count = 0;
1377
1378            for source_module in source_modules {
1379                if let Some(sym_id) = self.resolve_reexported_member_symbol_inner(
1380                    source_module,
1381                    member_name,
1382                    visited_aliases,
1383                    visited_modules,
1384                ) {
1385                    found_count += 1;
1386                    if found_count == 1 {
1387                        found_result = Some(sym_id);
1388                    } else {
1389                        // Multiple sources export the same name - ambiguous, treat as not exported
1390                        return None;
1391                    }
1392                }
1393            }
1394
1395            if found_result.is_some() {
1396                return found_result;
1397            }
1398        }
1399
1400        None
1401    }
1402
1403    // =========================================================================
1404    // Type Parameter Resolution
1405    // =========================================================================
1406
1407    /// Look up a type parameter by name in the current type parameter scope.
1408    ///
1409    /// Type parameters are scoped to their declaring generic (function, class, interface, etc.).
1410    /// This function checks the current type parameter scope to resolve type parameter names.
1411    pub(crate) fn lookup_type_parameter(&self, name: &str) -> Option<TypeId> {
1412        self.ctx.type_parameter_scope.get(name).copied()
1413    }
1414
1415    /// Get all type parameter bindings for passing to `TypeLowering`.
1416    ///
1417    /// Returns a vector of (name, `TypeId`) pairs for all type parameters in scope.
1418    pub(crate) fn get_type_param_bindings(&self) -> Vec<(tsz_common::interner::Atom, TypeId)> {
1419        self.ctx
1420            .type_parameter_scope
1421            .iter()
1422            .map(|(name, &type_id)| (self.ctx.types.intern_string(name), type_id))
1423            .collect()
1424    }
1425
1426    // =========================================================================
1427    // Entity Name Resolution
1428    // =========================================================================
1429
1430    /// Get the text representation of an entity name node.
1431    ///
1432    /// Get the text representation of an expression (simple chains only).
1433    /// Handles Identifiers and `PropertyAccessExpressions` (e.g., `a.b.c`).
1434    pub(crate) fn expression_text(&self, idx: NodeIndex) -> Option<String> {
1435        let node = self.ctx.arena.get(idx)?;
1436        match node.kind {
1437            k if k == SyntaxKind::Identifier as u16 => self
1438                .ctx
1439                .arena
1440                .get_identifier(node)
1441                .map(|ident| ident.escaped_text.clone()),
1442            k if k == syntax_kind_ext::PROPERTY_ACCESS_EXPRESSION => {
1443                let access = self.ctx.arena.get_access_expr(node)?;
1444                let left = self.expression_text(access.expression)?;
1445                let right = self.expression_text(access.name_or_argument)?;
1446                Some(format!("{left}.{right}"))
1447            }
1448            _ => None,
1449        }
1450    }
1451
1452    /// Entity names can be simple identifiers or qualified names (e.g., `A.B.C`).
1453    /// This function recursively builds the full text representation.
1454    pub(crate) fn entity_name_text(&self, idx: NodeIndex) -> Option<String> {
1455        let node = self.ctx.arena.get(idx)?;
1456        if node.kind == SyntaxKind::Identifier as u16 {
1457            return self
1458                .ctx
1459                .arena
1460                .get_identifier(node)
1461                .map(|ident| ident.escaped_text.clone());
1462        }
1463        if node.kind == syntax_kind_ext::QUALIFIED_NAME {
1464            let qn = self.ctx.arena.get_qualified_name(node)?;
1465            let left = self.entity_name_text(qn.left)?;
1466            let right = self.entity_name_text(qn.right)?;
1467            let mut combined = String::with_capacity(left.len() + 1 + right.len());
1468            combined.push_str(&left);
1469            combined.push('.');
1470            combined.push_str(&right);
1471            return Some(combined);
1472        }
1473        None
1474    }
1475
1476    // =========================================================================
1477    // Symbol Resolution for Lowering
1478    // =========================================================================
1479
1480    /// Resolve a type symbol for type lowering.
1481    ///
1482    /// Returns the symbol ID if the resolved symbol has the TYPE flag set.
1483    /// Returns None for built-in types that have special handling in `TypeLowering`.
1484    pub(crate) fn resolve_type_symbol_for_lowering(&self, idx: NodeIndex) -> Option<u32> {
1485        // Skip built-in types that have special handling in TypeLowering
1486        // These types use built-in TypeData representations instead of Refs
1487        if let Some(node) = self.ctx.arena.get(idx)
1488            && let Some(ident) = self.ctx.arena.get_identifier(node)
1489            && is_compiler_managed_type(ident.escaped_text.as_str())
1490        {
1491            return None;
1492        }
1493
1494        let sym_id = match self.resolve_qualified_symbol_in_type_position(idx) {
1495            TypeSymbolResolution::Type(sym_id) => sym_id,
1496            _ => return None,
1497        };
1498        let lib_binders = self.get_lib_binders();
1499        let symbol = self.ctx.binder.get_symbol_with_libs(sym_id, &lib_binders)?;
1500        ((symbol.flags & symbol_flags::TYPE) != 0).then_some(sym_id.0)
1501    }
1502
1503    /// Resolve a value symbol for type lowering.
1504    ///
1505    /// Returns the symbol ID if the resolved symbol has VALUE or ALIAS flags set.
1506    pub(crate) fn resolve_value_symbol_for_lowering(&self, idx: NodeIndex) -> Option<u32> {
1507        if let Some(node) = self.ctx.arena.get(idx) {
1508            if node.kind == SyntaxKind::Identifier as u16
1509                && let Some(sym_id) = self.resolve_identifier_symbol(idx)
1510                && self.alias_resolves_to_type_only(sym_id)
1511            {
1512                return None;
1513            }
1514            if node.kind == syntax_kind_ext::QUALIFIED_NAME {
1515                let mut current = idx;
1516                while let Some(node) = self.ctx.arena.get(current) {
1517                    if node.kind == SyntaxKind::Identifier as u16 {
1518                        if let Some(sym_id) = self.resolve_identifier_symbol(current)
1519                            && self.alias_resolves_to_type_only(sym_id)
1520                        {
1521                            return None;
1522                        }
1523                        break;
1524                    }
1525                    if node.kind != syntax_kind_ext::QUALIFIED_NAME {
1526                        break;
1527                    }
1528                    let Some(qn) = self.ctx.arena.get_qualified_name(node) else {
1529                        break;
1530                    };
1531                    current = qn.left;
1532                }
1533            }
1534        }
1535        let sym_id = self.resolve_qualified_symbol(idx)?;
1536        let lib_binders = self.get_lib_binders();
1537        let symbol = self.ctx.binder.get_symbol_with_libs(sym_id, &lib_binders)?;
1538        if symbol.is_type_only {
1539            return None;
1540        }
1541        if (symbol.flags & (symbol_flags::VALUE | symbol_flags::ALIAS)) != 0 {
1542            return Some(sym_id.0);
1543        }
1544
1545        // The initial resolution found a TYPE-only symbol (e.g., `interface Promise<T>`
1546        // from one lib file). But the VALUE declaration (`declare var Promise`) may
1547        // exist in a different lib file. Search all lib binders by name for a symbol
1548        // that has the VALUE flag. This handles declaration merging across lib files.
1549        let name = self
1550            .ctx
1551            .arena
1552            .get(idx)
1553            .and_then(|n| self.ctx.arena.get_identifier(n))
1554            .map(|i| i.escaped_text.as_str());
1555        if let Some(name) = name {
1556            // Check file_locals first (may have merged value from lib)
1557            if let Some(val_sym_id) = self.ctx.binder.file_locals.get(name)
1558                && let Some(val_symbol) = self
1559                    .ctx
1560                    .binder
1561                    .get_symbol_with_libs(val_sym_id, &lib_binders)
1562                && (val_symbol.flags & (symbol_flags::VALUE | symbol_flags::ALIAS)) != 0
1563                && !val_symbol.is_type_only
1564            {
1565                return Some(val_sym_id.0);
1566            }
1567            // Search lib binders directly for a value declaration
1568            for lib_binder in &lib_binders {
1569                if let Some(val_sym_id) = lib_binder.file_locals.get(name)
1570                    && let Some(val_symbol) = lib_binder.get_symbol(val_sym_id)
1571                    && (val_symbol.flags & (symbol_flags::VALUE | symbol_flags::ALIAS)) != 0
1572                    && !val_symbol.is_type_only
1573                {
1574                    return Some(val_sym_id.0);
1575                }
1576            }
1577        }
1578
1579        None
1580    }
1581}