Skip to main content

tsz_solver/objects/
element_access.rs

1use crate::evaluation::evaluate::{evaluate_index_access_with_options, evaluate_type};
2use crate::{LiteralValue, TypeData, TypeDatabase, TypeId};
3
4#[derive(Debug, Clone)]
5pub enum ElementAccessResult {
6    Success(TypeId),
7    NotIndexable {
8        type_id: TypeId,
9    },
10    IndexOutOfBounds {
11        type_id: TypeId,
12        index: usize,
13        length: usize,
14    },
15    NoIndexSignature {
16        type_id: TypeId,
17    },
18}
19
20pub struct ElementAccessEvaluator<'a> {
21    interner: &'a dyn TypeDatabase,
22    no_unchecked_indexed_access: bool,
23}
24
25impl<'a> ElementAccessEvaluator<'a> {
26    pub fn new(interner: &'a dyn TypeDatabase) -> Self {
27        Self {
28            interner,
29            no_unchecked_indexed_access: false,
30        }
31    }
32
33    pub const fn set_no_unchecked_indexed_access(&mut self, enabled: bool) {
34        self.no_unchecked_indexed_access = enabled;
35    }
36
37    pub fn resolve_element_access(
38        &self,
39        object_type: TypeId,
40        index_type: TypeId,
41        literal_index: Option<usize>,
42    ) -> ElementAccessResult {
43        // Evaluate object type first
44        let evaluated_object = evaluate_type(self.interner, object_type);
45
46        // Handle error/any
47        if evaluated_object == TypeId::ERROR || index_type == TypeId::ERROR {
48            return ElementAccessResult::Success(TypeId::ERROR);
49        }
50        if evaluated_object == TypeId::ANY {
51            return ElementAccessResult::Success(TypeId::ANY);
52        }
53
54        // Use the existing index access evaluator to get the type
55        let result_type = evaluate_index_access_with_options(
56            self.interner,
57            object_type,
58            index_type,
59            self.no_unchecked_indexed_access,
60        );
61
62        // 1. Check if object is indexable
63        if !self.is_indexable(evaluated_object) {
64            return ElementAccessResult::NotIndexable {
65                type_id: evaluated_object,
66            };
67        }
68
69        // 2. Check for Tuple out of bounds
70        if let Some(TypeData::Tuple(elements)) = self.interner.lookup(evaluated_object)
71            && let Some(index) = literal_index
72        {
73            let tuple_elements = self.interner.tuple_list(elements);
74
75            // Check bounds if no rest element
76            let has_rest = tuple_elements.iter().any(|e| e.rest);
77            if !has_rest && index >= tuple_elements.len() {
78                return ElementAccessResult::IndexOutOfBounds {
79                    type_id: evaluated_object,
80                    index,
81                    length: tuple_elements.len(),
82                };
83            }
84        }
85
86        // 3. Check for index signature (if not a specific property access)
87        if result_type == TypeId::UNDEFINED
88            && self.should_report_no_index_signature(evaluated_object, index_type)
89        {
90            return ElementAccessResult::NoIndexSignature {
91                type_id: evaluated_object,
92            };
93        }
94
95        ElementAccessResult::Success(result_type)
96    }
97
98    fn is_indexable(&self, type_id: TypeId) -> bool {
99        match self.interner.lookup(type_id) {
100            Some(
101                TypeData::Array(_)
102                | TypeData::Tuple(_)
103                | TypeData::Object(_)
104                | TypeData::ObjectWithIndex(_)
105                | TypeData::StringIntrinsic { .. }
106                | TypeData::Literal(LiteralValue::String(_))
107                | TypeData::Intersection(_),
108            ) => true,
109            Some(TypeData::Union(members)) => {
110                let members = self.interner.type_list(members);
111                members.iter().all(|&m| self.is_indexable(m))
112            }
113            _ => {
114                if type_id == TypeId::STRING || type_id == TypeId::ANY {
115                    return true;
116                }
117                false
118            }
119        }
120    }
121
122    fn should_report_no_index_signature(&self, object_type: TypeId, index_type: TypeId) -> bool {
123        let index_type = evaluate_type(self.interner, index_type);
124        // PERF: Reuse a single SubtypeChecker across all checks
125        let mut checker = crate::relations::subtype::SubtypeChecker::new(self.interner);
126        // Simplified check: checking if object has index signature compatible with index_type
127        match self.interner.lookup(object_type) {
128            Some(TypeData::Object(_)) => {
129                // Object without explicit index signature
130                // If index_type is string/number (not literal), then it's an error
131                // unless it's ANY
132                checker.reset();
133                if checker.is_subtype_of(index_type, TypeId::STRING) {
134                    return true;
135                }
136                checker.reset();
137                if checker.is_subtype_of(index_type, TypeId::NUMBER) {
138                    return true;
139                }
140                false
141            }
142            Some(TypeData::ObjectWithIndex(shape_id)) => {
143                let shape = self.interner.object_shape(shape_id);
144                checker.reset();
145                if checker.is_subtype_of(index_type, TypeId::STRING) && shape.string_index.is_none()
146                {
147                    return true;
148                }
149                // For number index, we can fallback to string index if present
150                checker.reset();
151                if checker.is_subtype_of(index_type, TypeId::NUMBER)
152                    && shape.number_index.is_none()
153                    && shape.string_index.is_none()
154                {
155                    return true;
156                }
157                false
158            }
159            _ => false,
160        }
161    }
162}