Skip to main content

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