Skip to main content

tsz_solver/evaluation/evaluate_rules/
index_access.rs

1//! Index access type evaluation.
2//!
3//! Handles TypeScript's index access types: `T[K]`
4//! Including property access, array indexing, and tuple indexing.
5
6use crate::instantiation::instantiate::{TypeSubstitution, instantiate_type};
7use crate::relations::subtype::TypeResolver;
8use crate::types::{
9    IntrinsicKind, LiteralValue, MappedModifier, MappedTypeId, ObjectShape, ObjectShapeId,
10    PropertyInfo, SymbolRef, TupleElement, TupleListId, TypeData, TypeId, TypeListId,
11    TypeParamInfo,
12};
13use crate::utils;
14use crate::visitor::{
15    TypeVisitor, array_element_type, literal_number, literal_string, tuple_list_id, union_list_id,
16};
17use crate::{ApparentMemberKind, TypeDatabase};
18
19use super::super::evaluate::{
20    ARRAY_METHODS_RETURN_ANY, ARRAY_METHODS_RETURN_BOOLEAN, ARRAY_METHODS_RETURN_NUMBER,
21    ARRAY_METHODS_RETURN_STRING, ARRAY_METHODS_RETURN_VOID, TypeEvaluator,
22};
23use super::apparent::make_apparent_method_type;
24
25fn is_member(name: &str, list: &[&str]) -> bool {
26    list.contains(&name)
27}
28
29/// Lazily compute and cache array member types (length + apparent methods).
30/// Shared between `ArrayKeyVisitor` and `TupleKeyVisitor`.
31fn get_or_init_array_member_types(
32    cache: &mut Option<Vec<TypeId>>,
33    db: &dyn TypeDatabase,
34) -> Vec<TypeId> {
35    cache
36        .get_or_insert_with(|| {
37            vec![
38                TypeId::NUMBER,
39                make_apparent_method_type(db, TypeId::ANY),
40                make_apparent_method_type(db, TypeId::BOOLEAN),
41                make_apparent_method_type(db, TypeId::NUMBER),
42                make_apparent_method_type(db, TypeId::VOID),
43                make_apparent_method_type(db, TypeId::STRING),
44            ]
45        })
46        .clone()
47}
48
49/// Standalone helper to get array member kind.
50/// Extracted from `TypeEvaluator` to be usable by visitors.
51pub(crate) fn get_array_member_kind(name: &str) -> Option<ApparentMemberKind> {
52    if name == "length" {
53        return Some(ApparentMemberKind::Value(TypeId::NUMBER));
54    }
55    if is_member(name, ARRAY_METHODS_RETURN_ANY) {
56        return Some(ApparentMemberKind::Method(TypeId::ANY));
57    }
58    if is_member(name, ARRAY_METHODS_RETURN_BOOLEAN) {
59        return Some(ApparentMemberKind::Method(TypeId::BOOLEAN));
60    }
61    if is_member(name, ARRAY_METHODS_RETURN_NUMBER) {
62        return Some(ApparentMemberKind::Method(TypeId::NUMBER));
63    }
64    if is_member(name, ARRAY_METHODS_RETURN_VOID) {
65        return Some(ApparentMemberKind::Method(TypeId::VOID));
66    }
67    if is_member(name, ARRAY_METHODS_RETURN_STRING) {
68        return Some(ApparentMemberKind::Method(TypeId::STRING));
69    }
70    None
71}
72
73struct IndexAccessVisitor<'a, 'b, R: TypeResolver> {
74    evaluator: &'b mut TypeEvaluator<'a, R>,
75    object_type: TypeId,
76    index_type: TypeId,
77}
78
79impl<'a, 'b, R: TypeResolver> IndexAccessVisitor<'a, 'b, R> {
80    fn evaluate_apparent_primitive(&mut self, kind: IntrinsicKind) -> Option<TypeId> {
81        match kind {
82            IntrinsicKind::String
83            | IntrinsicKind::Number
84            | IntrinsicKind::Boolean
85            | IntrinsicKind::Bigint
86            | IntrinsicKind::Symbol => {
87                let shape = self.evaluator.apparent_primitive_shape(kind);
88                Some(
89                    self.evaluator
90                        .evaluate_object_with_index(&shape, self.index_type),
91                )
92            }
93            _ => None,
94        }
95    }
96
97    /// Check if the index type is generic (deferrable).
98    ///
99    /// When evaluating an index access during generic instantiation,
100    /// if the index is still a generic type (like a type parameter),
101    /// we must defer evaluation instead of returning UNDEFINED.
102    fn is_generic_index(&self) -> bool {
103        let key = match self.evaluator.interner().lookup(self.index_type) {
104            Some(k) => k,
105            None => return false,
106        };
107
108        matches!(
109            key,
110            TypeData::TypeParameter(_)
111                | TypeData::Infer(_)
112                | TypeData::KeyOf(_)
113                | TypeData::IndexAccess(_, _)
114                | TypeData::Conditional(_)
115                | TypeData::TemplateLiteral(_) // Templates might resolve to generic strings
116                | TypeData::Intersection(_)
117        )
118    }
119
120    fn evaluate_type_param(&mut self, param: &TypeParamInfo) -> Option<TypeId> {
121        if let Some(constraint) = param.constraint {
122            if constraint == self.object_type {
123                Some(
124                    self.evaluator
125                        .interner()
126                        .index_access(self.object_type, self.index_type),
127                )
128            } else {
129                Some(
130                    self.evaluator
131                        .recurse_index_access(constraint, self.index_type),
132                )
133            }
134        } else {
135            Some(
136                self.evaluator
137                    .interner()
138                    .index_access(self.object_type, self.index_type),
139            )
140        }
141    }
142}
143
144impl<'a, 'b, R: TypeResolver> TypeVisitor for IndexAccessVisitor<'a, 'b, R> {
145    type Output = Option<TypeId>;
146
147    fn visit_intrinsic(&mut self, kind: IntrinsicKind) -> Self::Output {
148        self.evaluate_apparent_primitive(kind)
149    }
150
151    fn visit_literal(&mut self, value: &LiteralValue) -> Self::Output {
152        self.evaluator
153            .apparent_literal_kind(value)
154            .and_then(|kind| self.evaluate_apparent_primitive(kind))
155    }
156
157    fn visit_object(&mut self, shape_id: u32) -> Self::Output {
158        let shape = self
159            .evaluator
160            .interner()
161            .object_shape(ObjectShapeId(shape_id));
162
163        let result = self
164            .evaluator
165            .evaluate_object_index(&shape.properties, self.index_type);
166
167        // CRITICAL FIX: If we can't find the property, but the index is generic,
168        // we must defer evaluation (return None) instead of returning UNDEFINED.
169        // This prevents mapped type template evaluation from hardcoding UNDEFINED
170        // during generic instantiation.
171        if result == TypeId::UNDEFINED && self.is_generic_index() {
172            return None;
173        }
174
175        Some(result)
176    }
177
178    fn visit_object_with_index(&mut self, shape_id: u32) -> Self::Output {
179        let shape = self
180            .evaluator
181            .interner()
182            .object_shape(ObjectShapeId(shape_id));
183
184        let result = self
185            .evaluator
186            .evaluate_object_with_index(&shape, self.index_type);
187
188        // CRITICAL FIX: Same deferral logic for objects with index signatures
189        if result == TypeId::UNDEFINED && self.is_generic_index() {
190            return None;
191        }
192
193        Some(result)
194    }
195
196    fn visit_union(&mut self, list_id: u32) -> Self::Output {
197        let members = self.evaluator.interner().type_list(TypeListId(list_id));
198        const MAX_UNION_INDEX_SIZE: usize = 100;
199        if members.len() > MAX_UNION_INDEX_SIZE {
200            self.evaluator.mark_depth_exceeded();
201            return Some(TypeId::ERROR);
202        }
203        let mut results = Vec::new();
204        for &member in members.iter() {
205            if self.evaluator.is_depth_exceeded() {
206                return Some(TypeId::ERROR);
207            }
208            let result = self.evaluator.recurse_index_access(member, self.index_type);
209            if result == TypeId::ERROR && self.evaluator.is_depth_exceeded() {
210                return Some(TypeId::ERROR);
211            }
212            if result != TypeId::UNDEFINED || self.evaluator.no_unchecked_indexed_access() {
213                results.push(result);
214            }
215        }
216        if results.is_empty() {
217            return Some(TypeId::UNDEFINED);
218        }
219        Some(self.evaluator.interner().union(results))
220    }
221
222    fn visit_intersection(&mut self, list_id: u32) -> Self::Output {
223        // For intersection types, evaluate all members and combine successful lookups.
224        // Returning the first non-undefined result can incorrectly lock onto `never`
225        // for mapped/index-signature helper intersections.
226        let members = self.evaluator.interner().type_list(TypeListId(list_id));
227        let mut results = Vec::new();
228        for &member in members.iter() {
229            let result = self.evaluator.recurse_index_access(member, self.index_type);
230            if result == TypeId::ERROR {
231                return Some(TypeId::ERROR);
232            }
233            if result != TypeId::UNDEFINED {
234                results.push(result);
235            }
236        }
237        if results.is_empty() {
238            Some(TypeId::UNDEFINED)
239        } else {
240            Some(self.evaluator.interner().union(results))
241        }
242    }
243
244    fn visit_lazy(&mut self, def_id: u32) -> Self::Output {
245        // CRITICAL: Classes and interfaces are represented as Lazy types.
246        // We must resolve them and then perform the index access lookup.
247        let def_id = crate::def::DefId(def_id);
248        if let Some(resolved) = self
249            .evaluator
250            .resolver()
251            .resolve_lazy(def_id, self.evaluator.interner())
252        {
253            // CRITICAL: Use evaluate_index_access directly (not recurse) to perform property lookup
254            // This resolves the class C and then finds the "foo" property within it
255            return Some(
256                self.evaluator
257                    .evaluate_index_access(resolved, self.index_type),
258            );
259        }
260        None
261    }
262
263    fn visit_array(&mut self, element_type: TypeId) -> Self::Output {
264        Some(
265            self.evaluator
266                .evaluate_array_index(element_type, self.index_type),
267        )
268    }
269
270    fn visit_tuple(&mut self, list_id: u32) -> Self::Output {
271        let elements = self.evaluator.interner().tuple_list(TupleListId(list_id));
272        Some(
273            self.evaluator
274                .evaluate_tuple_index(&elements, self.index_type),
275        )
276    }
277
278    fn visit_ref(&mut self, symbol_ref: u32) -> Self::Output {
279        let symbol_ref = SymbolRef(symbol_ref);
280        let resolved = if let Some(def_id) = self.evaluator.resolver().symbol_to_def_id(symbol_ref)
281        {
282            self.evaluator
283                .resolver()
284                .resolve_lazy(def_id, self.evaluator.interner())?
285        } else {
286            self.evaluator
287                .resolver()
288                .resolve_symbol_ref(symbol_ref, self.evaluator.interner())?
289        };
290        if resolved == self.object_type {
291            Some(
292                self.evaluator
293                    .interner()
294                    .index_access(self.object_type, self.index_type),
295            )
296        } else {
297            Some(
298                self.evaluator
299                    .recurse_index_access(resolved, self.index_type),
300            )
301        }
302    }
303
304    fn visit_type_parameter(&mut self, param_info: &TypeParamInfo) -> Self::Output {
305        self.evaluate_type_param(param_info)
306    }
307
308    fn visit_infer(&mut self, param_info: &TypeParamInfo) -> Self::Output {
309        self.evaluate_type_param(param_info)
310    }
311
312    fn visit_readonly_type(&mut self, inner_type: TypeId) -> Self::Output {
313        Some(
314            self.evaluator
315                .recurse_index_access(inner_type, self.index_type),
316        )
317    }
318
319    fn visit_mapped(&mut self, mapped_id: u32) -> Self::Output {
320        let mapped = self
321            .evaluator
322            .interner()
323            .mapped_type(MappedTypeId(mapped_id));
324
325        // Optimization: Mapped[K] -> Template[P/K] where K matches constraint
326        // This handles cases like `Ev<K>["callback"]` where Ev<K> is a mapped type
327        // over K, without needing to expand the mapped type (which fails for TypeParameter K).
328
329        // Only apply if no name remapping (as clause)
330        if mapped.name_type.is_none() && mapped.constraint == self.index_type {
331            let mut subst = TypeSubstitution::new();
332            subst.insert(mapped.type_param.name, self.index_type);
333
334            let mut value_type = self.evaluator.evaluate(instantiate_type(
335                self.evaluator.interner(),
336                mapped.template,
337                &subst,
338            ));
339
340            // Handle optional modifier
341            if matches!(mapped.optional_modifier, Some(MappedModifier::Add)) {
342                value_type = self
343                    .evaluator
344                    .interner()
345                    .union2(value_type, TypeId::UNDEFINED);
346            }
347
348            return Some(value_type);
349        }
350
351        None
352    }
353
354    fn visit_template_literal(&mut self, _template_id: u32) -> Self::Output {
355        self.evaluate_apparent_primitive(IntrinsicKind::String)
356    }
357
358    fn default_output() -> Self::Output {
359        None
360    }
361}
362
363// =============================================================================
364// Visitor Pattern Implementations for Index Type Evaluation
365// =============================================================================
366
367/// Visitor to handle array index access: `Array[K]`
368///
369/// Evaluates what type is returned when indexing an array with various key types.
370/// Uses Option<TypeId> to signal "use default fallback" via None.
371struct ArrayKeyVisitor<'a> {
372    db: &'a dyn TypeDatabase,
373    element_type: TypeId,
374    array_member_types_cache: Option<Vec<TypeId>>,
375}
376
377impl<'a> ArrayKeyVisitor<'a> {
378    fn new(db: &'a dyn TypeDatabase, element_type: TypeId) -> Self {
379        Self {
380            db,
381            element_type,
382            array_member_types_cache: None,
383        }
384    }
385
386    /// Driver method that handles the fallback logic
387    fn evaluate(&mut self, index_type: TypeId) -> TypeId {
388        let result = self.visit_type(self.db, index_type);
389        result.unwrap_or(self.element_type)
390    }
391
392    fn get_array_member_types(&mut self) -> Vec<TypeId> {
393        get_or_init_array_member_types(&mut self.array_member_types_cache, self.db)
394    }
395}
396
397impl<'a> TypeVisitor for ArrayKeyVisitor<'a> {
398    type Output = Option<TypeId>;
399
400    fn visit_union(&mut self, list_id: u32) -> Self::Output {
401        let members = self.db.type_list(TypeListId(list_id));
402        let mut results = Vec::new();
403        for &member in members.iter() {
404            let result = self.evaluate(member);
405            if result != TypeId::UNDEFINED {
406                results.push(result);
407            }
408        }
409        if results.is_empty() {
410            Some(TypeId::UNDEFINED)
411        } else {
412            Some(self.db.union(results))
413        }
414    }
415
416    fn visit_intrinsic(&mut self, kind: IntrinsicKind) -> Self::Output {
417        match kind {
418            IntrinsicKind::Number => Some(self.element_type),
419            IntrinsicKind::String => Some(self.db.union(self.get_array_member_types())),
420            _ => Some(TypeId::UNDEFINED),
421        }
422    }
423
424    fn visit_literal(&mut self, value: &LiteralValue) -> Self::Output {
425        match value {
426            LiteralValue::Number(_) => Some(self.element_type),
427            LiteralValue::String(atom) => {
428                let name = self.db.resolve_atom_ref(*atom);
429                if utils::is_numeric_property_name(self.db, *atom) {
430                    return Some(self.element_type);
431                }
432                // Check for known array members
433                if let Some(member) = get_array_member_kind(name.as_ref()) {
434                    return match member {
435                        ApparentMemberKind::Value(type_id) => Some(type_id),
436                        ApparentMemberKind::Method(return_type) => {
437                            Some(make_apparent_method_type(self.db, return_type))
438                        }
439                    };
440                }
441                Some(TypeId::UNDEFINED)
442            }
443            // Explicitly handle other literals to avoid incorrect fallback
444            LiteralValue::Boolean(_) | LiteralValue::BigInt(_) => Some(TypeId::UNDEFINED),
445        }
446    }
447
448    /// Signal "use the default fallback" for unhandled type variants
449    fn default_output() -> Self::Output {
450        None
451    }
452}
453
454/// Visitor to handle tuple index access: `Tuple[K]`
455///
456/// Evaluates what type is returned when indexing a tuple with various key types.
457/// Uses Option<TypeId> to signal "use default fallback" via None.
458struct TupleKeyVisitor<'a> {
459    db: &'a dyn TypeDatabase,
460    elements: &'a [TupleElement],
461    array_member_types_cache: Option<Vec<TypeId>>,
462}
463
464impl<'a> TupleKeyVisitor<'a> {
465    fn new(db: &'a dyn TypeDatabase, elements: &'a [TupleElement]) -> Self {
466        Self {
467            db,
468            elements,
469            array_member_types_cache: None,
470        }
471    }
472
473    /// Driver method that handles the fallback logic
474    fn evaluate(&mut self, index_type: TypeId) -> TypeId {
475        let result = self.visit_type(self.db, index_type);
476        result.unwrap_or(TypeId::UNDEFINED)
477    }
478
479    /// Get the type of a tuple element, handling optional and rest elements
480    fn tuple_element_type(&self, element: &TupleElement) -> TypeId {
481        let mut type_id = if element.rest {
482            self.rest_element_type(element.type_id)
483        } else {
484            element.type_id
485        };
486
487        if element.optional {
488            type_id = self.db.union2(type_id, TypeId::UNDEFINED);
489        }
490
491        type_id
492    }
493
494    /// Get the element type of a rest element (handles nested rest and array types)
495    fn rest_element_type(&self, type_id: TypeId) -> TypeId {
496        if let Some(elem) = array_element_type(self.db, type_id) {
497            return elem;
498        }
499        if let Some(elements) = tuple_list_id(self.db, type_id) {
500            let elements = self.db.tuple_list(elements);
501            let types: Vec<TypeId> = elements
502                .iter()
503                .map(|e| self.tuple_element_type(e))
504                .collect();
505            if types.is_empty() {
506                TypeId::NEVER
507            } else {
508                self.db.union(types)
509            }
510        } else {
511            type_id
512        }
513    }
514
515    /// Get the type at a specific literal index, handling rest elements
516    fn tuple_index_literal(&self, idx: usize) -> Option<TypeId> {
517        for (logical_idx, element) in self.elements.iter().enumerate() {
518            if element.rest {
519                if let Some(rest_elements) = tuple_list_id(self.db, element.type_id) {
520                    let rest_elements = self.db.tuple_list(rest_elements);
521                    let inner_idx = idx.saturating_sub(logical_idx);
522                    // Recursively search in rest elements
523                    let inner_visitor = TupleKeyVisitor::new(self.db, &rest_elements);
524                    return inner_visitor.tuple_index_literal(inner_idx);
525                }
526                return Some(self.tuple_element_type(element));
527            }
528
529            if logical_idx == idx {
530                return Some(self.tuple_element_type(element));
531            }
532        }
533
534        None
535    }
536
537    /// Get all tuple element types as a union
538    fn get_all_element_types(&self) -> Vec<TypeId> {
539        self.elements
540            .iter()
541            .map(|e| self.tuple_element_type(e))
542            .collect()
543    }
544
545    /// Get array member types (cached)
546    fn get_array_member_types(&mut self) -> Vec<TypeId> {
547        get_or_init_array_member_types(&mut self.array_member_types_cache, self.db)
548    }
549
550    /// Check for known array members (length, methods)
551    fn get_array_member_kind(&self, name: &str) -> Option<ApparentMemberKind> {
552        if name == "length" {
553            return Some(ApparentMemberKind::Value(TypeId::NUMBER));
554        }
555        if is_member(name, ARRAY_METHODS_RETURN_ANY) {
556            return Some(ApparentMemberKind::Method(TypeId::ANY));
557        }
558        if is_member(name, ARRAY_METHODS_RETURN_BOOLEAN) {
559            return Some(ApparentMemberKind::Method(TypeId::BOOLEAN));
560        }
561        if is_member(name, ARRAY_METHODS_RETURN_NUMBER) {
562            return Some(ApparentMemberKind::Method(TypeId::NUMBER));
563        }
564        if is_member(name, ARRAY_METHODS_RETURN_VOID) {
565            return Some(ApparentMemberKind::Method(TypeId::VOID));
566        }
567        if is_member(name, ARRAY_METHODS_RETURN_STRING) {
568            return Some(ApparentMemberKind::Method(TypeId::STRING));
569        }
570        None
571    }
572}
573
574impl<'a> TypeVisitor for TupleKeyVisitor<'a> {
575    type Output = Option<TypeId>;
576
577    fn visit_union(&mut self, list_id: u32) -> Self::Output {
578        let members = self.db.type_list(TypeListId(list_id));
579        let mut results = Vec::new();
580        for &member in members.iter() {
581            let result = self.evaluate(member);
582            if result != TypeId::UNDEFINED {
583                results.push(result);
584            }
585        }
586        if results.is_empty() {
587            Some(TypeId::UNDEFINED)
588        } else {
589            Some(self.db.union(results))
590        }
591    }
592
593    fn visit_intrinsic(&mut self, kind: IntrinsicKind) -> Self::Output {
594        match kind {
595            IntrinsicKind::String => {
596                // Return union of all element types + array member types
597                let mut types = self.get_all_element_types();
598                types.extend(self.get_array_member_types());
599                if types.is_empty() {
600                    Some(TypeId::NEVER)
601                } else {
602                    Some(self.db.union(types))
603                }
604            }
605            IntrinsicKind::Number => {
606                // Return union of all element types
607                let all_types = self.get_all_element_types();
608                if all_types.is_empty() {
609                    Some(TypeId::NEVER)
610                } else {
611                    Some(self.db.union(all_types))
612                }
613            }
614            _ => Some(TypeId::UNDEFINED),
615        }
616    }
617
618    fn visit_literal(&mut self, value: &LiteralValue) -> Self::Output {
619        match value {
620            LiteralValue::Number(n) => {
621                let value = n.0;
622                if !value.is_finite() || value.fract() != 0.0 || value < 0.0 {
623                    return Some(TypeId::UNDEFINED);
624                }
625                let idx = value as usize;
626                self.tuple_index_literal(idx).or(Some(TypeId::UNDEFINED))
627            }
628            LiteralValue::String(atom) => {
629                // Check if it's a numeric property name (e.g., "0", "1", "42")
630                if utils::is_numeric_property_name(self.db, *atom) {
631                    let name = self.db.resolve_atom_ref(*atom);
632                    if let Ok(idx) = name.as_ref().parse::<i64>()
633                        && let Ok(idx) = usize::try_from(idx)
634                    {
635                        return self.tuple_index_literal(idx).or(Some(TypeId::UNDEFINED));
636                    }
637                    return Some(TypeId::UNDEFINED);
638                }
639
640                // Check for known array members
641                let name = self.db.resolve_atom_ref(*atom);
642                if let Some(member) = self.get_array_member_kind(name.as_ref()) {
643                    return match member {
644                        ApparentMemberKind::Value(type_id) => Some(type_id),
645                        ApparentMemberKind::Method(return_type) => {
646                            Some(make_apparent_method_type(self.db, return_type))
647                        }
648                    };
649                }
650
651                Some(TypeId::UNDEFINED)
652            }
653            // Explicitly handle other literals to avoid incorrect fallback
654            LiteralValue::Boolean(_) | LiteralValue::BigInt(_) => Some(TypeId::UNDEFINED),
655        }
656    }
657
658    /// Signal "use the default fallback" for unhandled type variants
659    fn default_output() -> Self::Output {
660        None
661    }
662}
663
664impl<'a, R: TypeResolver> TypeEvaluator<'a, R> {
665    /// Helper to recursively evaluate an index access while respecting depth limits.
666    /// Creates an `IndexAccess` type and evaluates it through the main `evaluate()` method.
667    pub(crate) fn recurse_index_access(
668        &mut self,
669        object_type: TypeId,
670        index_type: TypeId,
671    ) -> TypeId {
672        let index_access = self.interner().index_access(object_type, index_type);
673        self.evaluate(index_access)
674    }
675
676    /// Evaluate an index access type: T[K]
677    ///
678    /// This resolves property access on object types.
679    pub fn evaluate_index_access(&mut self, object_type: TypeId, index_type: TypeId) -> TypeId {
680        let evaluated_object = self.evaluate(object_type);
681        let evaluated_index = self.evaluate(index_type);
682        if evaluated_object != object_type || evaluated_index != index_type {
683            // Use recurse_index_access to respect depth limits
684            return self.recurse_index_access(evaluated_object, evaluated_index);
685        }
686        // Match tsc: index access involving `any` produces `any`.
687        // (e.g. `any[string]` is `any`, not an error)
688        if evaluated_object == TypeId::ANY || evaluated_index == TypeId::ANY {
689            return TypeId::ANY;
690        }
691
692        // Rule #38: Distribute over index union at the top level (Cartesian product expansion)
693        // T[A | B] -> T[A] | T[B]
694        // This must happen before checking the object type to ensure full cross-product expansion
695        // when both object and index are unions: (X | Y)[A | B] -> X[A] | X[B] | Y[A] | Y[B]
696        if let Some(members_id) = union_list_id(self.interner(), index_type) {
697            let members = self.interner().type_list(members_id);
698            // Limit to prevent OOM with large unions
699            const MAX_UNION_INDEX_SIZE: usize = 100;
700            if members.len() > MAX_UNION_INDEX_SIZE {
701                self.mark_depth_exceeded();
702                return TypeId::ERROR;
703            }
704            let mut results = Vec::new();
705            for &member in members.iter() {
706                if self.is_depth_exceeded() {
707                    return TypeId::ERROR;
708                }
709                let result = self.recurse_index_access(object_type, member);
710                if result == TypeId::ERROR && self.is_depth_exceeded() {
711                    return TypeId::ERROR;
712                }
713                if result != TypeId::UNDEFINED || self.no_unchecked_indexed_access() {
714                    results.push(result);
715                }
716            }
717            if results.is_empty() {
718                return TypeId::UNDEFINED;
719            }
720            return self.interner().union(results);
721        }
722
723        let interner = self.interner();
724        let mut visitor = IndexAccessVisitor {
725            evaluator: self,
726            object_type,
727            index_type,
728        };
729        if let Some(result) = visitor.visit_type(interner, object_type) {
730            return result;
731        }
732
733        // For other types, keep as IndexAccess (deferred)
734        self.interner().index_access(object_type, index_type)
735    }
736
737    /// Evaluate property access on an object type
738    pub(crate) fn evaluate_object_index(
739        &self,
740        props: &[PropertyInfo],
741        index_type: TypeId,
742    ) -> TypeId {
743        // If index is a literal string, look up the property directly
744        if let Some(name) = literal_string(self.interner(), index_type) {
745            for prop in props {
746                if prop.name == name {
747                    return self.optional_property_type(prop);
748                }
749            }
750            // Property not found
751            return TypeId::UNDEFINED;
752        }
753
754        // If index is a union of literals, return union of property types
755        if let Some(members) = union_list_id(self.interner(), index_type) {
756            let members = self.interner().type_list(members);
757            let mut results = Vec::new();
758            for &member in members.iter() {
759                let result = self.evaluate_object_index(props, member);
760                if result != TypeId::UNDEFINED || self.no_unchecked_indexed_access() {
761                    results.push(result);
762                }
763            }
764            if results.is_empty() {
765                return TypeId::UNDEFINED;
766            }
767            return self.interner().union(results);
768        }
769
770        // If index is string, return union of all property types (index signature behavior)
771        if index_type == TypeId::STRING {
772            let union = self.union_property_types(props);
773            return self.add_undefined_if_unchecked(union);
774        }
775
776        TypeId::UNDEFINED
777    }
778
779    /// Evaluate property access on an object type with index signatures.
780    pub(crate) fn evaluate_object_with_index(
781        &self,
782        shape: &ObjectShape,
783        index_type: TypeId,
784    ) -> TypeId {
785        // If index is a union, evaluate each member
786        if let Some(members) = union_list_id(self.interner(), index_type) {
787            let members = self.interner().type_list(members);
788            let mut results = Vec::new();
789            for &member in members.iter() {
790                let result = self.evaluate_object_with_index(shape, member);
791                if result != TypeId::UNDEFINED || self.no_unchecked_indexed_access() {
792                    results.push(result);
793                }
794            }
795            if results.is_empty() {
796                return TypeId::UNDEFINED;
797            }
798            return self.interner().union(results);
799        }
800
801        // If index is a literal string, look up the property first, then fallback to string index.
802        if let Some(name) = literal_string(self.interner(), index_type) {
803            for prop in &shape.properties {
804                if prop.name == name {
805                    return self.optional_property_type(prop);
806                }
807            }
808            if utils::is_numeric_property_name(self.interner(), name)
809                && let Some(number_index) = shape.number_index.as_ref()
810            {
811                return self.add_undefined_if_unchecked(number_index.value_type);
812            }
813            if let Some(string_index) = shape.string_index.as_ref() {
814                return self.add_undefined_if_unchecked(string_index.value_type);
815            }
816            return TypeId::UNDEFINED;
817        }
818
819        // If index is a literal number, prefer number index, then string index.
820        if literal_number(self.interner(), index_type).is_some() {
821            if let Some(number_index) = shape.number_index.as_ref() {
822                return self.add_undefined_if_unchecked(number_index.value_type);
823            }
824            if let Some(string_index) = shape.string_index.as_ref() {
825                return self.add_undefined_if_unchecked(string_index.value_type);
826            }
827            return TypeId::UNDEFINED;
828        }
829
830        if index_type == TypeId::STRING {
831            let result = if let Some(string_index) = shape.string_index.as_ref() {
832                string_index.value_type
833            } else {
834                self.union_property_types(&shape.properties)
835            };
836            return self.add_undefined_if_unchecked(result);
837        }
838
839        if index_type == TypeId::NUMBER {
840            let result = if let Some(number_index) = shape.number_index.as_ref() {
841                number_index.value_type
842            } else if let Some(string_index) = shape.string_index.as_ref() {
843                string_index.value_type
844            } else {
845                self.union_property_types(&shape.properties)
846            };
847            return self.add_undefined_if_unchecked(result);
848        }
849
850        TypeId::UNDEFINED
851    }
852
853    pub(crate) fn union_property_types(&self, props: &[PropertyInfo]) -> TypeId {
854        let all_types: Vec<TypeId> = props
855            .iter()
856            .map(|prop| self.optional_property_type(prop))
857            .collect();
858        if all_types.is_empty() {
859            TypeId::UNDEFINED
860        } else {
861            self.interner().union(all_types)
862        }
863    }
864
865    pub(crate) fn optional_property_type(&self, prop: &PropertyInfo) -> TypeId {
866        crate::utils::optional_property_type(self.interner(), prop)
867    }
868
869    pub(crate) fn add_undefined_if_unchecked(&self, type_id: TypeId) -> TypeId {
870        if !self.no_unchecked_indexed_access() || type_id == TypeId::UNDEFINED {
871            return type_id;
872        }
873        self.interner().union2(type_id, TypeId::UNDEFINED)
874    }
875
876    pub(crate) fn rest_element_type(&self, type_id: TypeId) -> TypeId {
877        if let Some(elem) = array_element_type(self.interner(), type_id) {
878            return elem;
879        }
880        if let Some(elements) = tuple_list_id(self.interner(), type_id) {
881            let elements = self.interner().tuple_list(elements);
882            let types: Vec<TypeId> = elements
883                .iter()
884                .map(|e| self.tuple_element_type(e))
885                .collect();
886            if types.is_empty() {
887                TypeId::NEVER
888            } else {
889                self.interner().union(types)
890            }
891        } else {
892            type_id
893        }
894    }
895
896    pub(crate) fn tuple_element_type(&self, element: &TupleElement) -> TypeId {
897        let mut type_id = if element.rest {
898            self.rest_element_type(element.type_id)
899        } else {
900            element.type_id
901        };
902
903        if element.optional {
904            type_id = self.interner().union2(type_id, TypeId::UNDEFINED);
905        }
906
907        type_id
908    }
909
910    /// Evaluate index access on a tuple type
911    pub(crate) fn evaluate_tuple_index(
912        &self,
913        elements: &[TupleElement],
914        index_type: TypeId,
915    ) -> TypeId {
916        // Use TupleKeyVisitor to handle the index type
917        // The visitor handles Union distribution internally via visit_union
918        let mut visitor = TupleKeyVisitor::new(self.interner(), elements);
919        let result = visitor.evaluate(index_type);
920
921        // Add undefined if unchecked indexed access is allowed
922        self.add_undefined_if_unchecked(result)
923    }
924
925    pub(crate) fn evaluate_array_index(&self, elem: TypeId, index_type: TypeId) -> TypeId {
926        // Use ArrayKeyVisitor to handle the index type
927        // The visitor handles Union distribution internally via visit_union
928        let mut visitor = ArrayKeyVisitor::new(self.interner(), elem);
929        let result = visitor.evaluate(index_type);
930
931        // Add undefined if unchecked indexed access is allowed
932        self.add_undefined_if_unchecked(result)
933    }
934}