Skip to main content

tsz_solver/objects/
index_signatures.rs

1//! Index Signature Resolution
2//!
3//! This module provides a unified interface for querying and resolving
4//! index signatures on object types. Index signatures allow objects to be
5//! accessed using string or numeric keys (e.g., `{ [key: string]: number }`).
6//!
7//! ## Key Types
8//!
9//! - **`IndexKind`**: Distinguishes between string and numeric index signatures
10//! - **`IndexSignatureResolver`**: Main resolver for index signature queries
11//!
12//! ## Usage
13//!
14//! ```rust,ignore
15//! use crate::objects::index_signatures::IndexSignatureResolver;
16//!
17//! let resolver = IndexSignatureResolver::new(db);
18//!
19//! // Get string index signature type
20//! if let Some(value_type) = resolver.resolve_string_index(obj_type) {
21//!     // Object has string index signature
22//! }
23//!
24//! // Check if index signature is readonly
25//! if resolver.is_readonly(obj_type, IndexKind::String) {
26//!     // Index signature is readonly
27//! }
28//! ```
29
30use crate::types::{IndexInfo, IndexSignature, ObjectShapeId};
31use crate::utils;
32use crate::visitor::TypeVisitor;
33use crate::{TypeDatabase, TypeId};
34
35/// Distinguishes between string and numeric index signatures.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum IndexKind {
38    /// String index signature: `{ [key: string]: T }`
39    String,
40    /// Numeric index signature: `{ [key: number]: T }`
41    Number,
42}
43
44// =============================================================================
45// Visitor Implementations for Index Signature Resolution
46// =============================================================================
47
48/// Visitor for resolving string index signatures.
49struct StringIndexResolver<'a> {
50    db: &'a dyn TypeDatabase,
51}
52
53impl<'a> TypeVisitor for StringIndexResolver<'a> {
54    type Output = Option<TypeId>;
55
56    fn visit_intrinsic(&mut self, _kind: crate::types::IntrinsicKind) -> Self::Output {
57        None
58    }
59
60    fn visit_literal(&mut self, _value: &crate::LiteralValue) -> Self::Output {
61        None
62    }
63
64    fn visit_object_with_index(&mut self, shape_id: u32) -> Self::Output {
65        let shape = self.db.object_shape(ObjectShapeId(shape_id));
66        shape.string_index.as_ref().map(|idx| idx.value_type)
67    }
68
69    fn visit_array(&mut self, element_type: TypeId) -> Self::Output {
70        // Array/tuple types have readonly numeric index (which also supports string)
71        Some(element_type)
72    }
73
74    fn visit_tuple(&mut self, _list_id: u32) -> Self::Output {
75        // Would need union of all elements, return UNKNOWN for simplicity
76        Some(TypeId::UNKNOWN)
77    }
78
79    fn visit_union(&mut self, list_id: u32) -> Self::Output {
80        let types = self.db.type_list(crate::types::TypeListId(list_id));
81        types.iter().find_map(|&t| self.visit_type(self.db, t))
82    }
83
84    fn visit_intersection(&mut self, list_id: u32) -> Self::Output {
85        let types = self.db.type_list(crate::types::TypeListId(list_id));
86        // For intersection, return the first one found
87        types.first().and_then(|&t| self.visit_type(self.db, t))
88    }
89
90    fn visit_readonly_type(&mut self, inner_type: TypeId) -> Self::Output {
91        self.visit_type(self.db, inner_type)
92    }
93
94    fn default_output() -> Self::Output {
95        None
96    }
97}
98
99/// Visitor for resolving number index signatures.
100struct NumberIndexResolver<'a> {
101    db: &'a dyn TypeDatabase,
102}
103
104impl<'a> TypeVisitor for NumberIndexResolver<'a> {
105    type Output = Option<TypeId>;
106
107    fn visit_intrinsic(&mut self, _kind: crate::types::IntrinsicKind) -> Self::Output {
108        None
109    }
110
111    fn visit_literal(&mut self, _value: &crate::LiteralValue) -> Self::Output {
112        None
113    }
114
115    fn visit_object_with_index(&mut self, shape_id: u32) -> Self::Output {
116        let shape = self.db.object_shape(ObjectShapeId(shape_id));
117        shape.number_index.as_ref().map(|idx| idx.value_type)
118    }
119
120    fn visit_array(&mut self, element_type: TypeId) -> Self::Output {
121        Some(element_type)
122    }
123
124    fn visit_tuple(&mut self, _list_id: u32) -> Self::Output {
125        Some(TypeId::UNKNOWN)
126    }
127
128    fn visit_union(&mut self, list_id: u32) -> Self::Output {
129        let types = self.db.type_list(crate::types::TypeListId(list_id));
130        types.iter().find_map(|&t| self.visit_type(self.db, t))
131    }
132
133    fn visit_intersection(&mut self, list_id: u32) -> Self::Output {
134        let types = self.db.type_list(crate::types::TypeListId(list_id));
135        types.first().and_then(|&t| self.visit_type(self.db, t))
136    }
137
138    fn visit_readonly_type(&mut self, inner_type: TypeId) -> Self::Output {
139        self.visit_type(self.db, inner_type)
140    }
141
142    fn default_output() -> Self::Output {
143        None
144    }
145}
146
147/// Visitor for checking if an index signature is readonly.
148struct ReadonlyChecker<'a> {
149    db: &'a dyn TypeDatabase,
150    kind: IndexKind,
151}
152
153impl<'a> TypeVisitor for ReadonlyChecker<'a> {
154    type Output = bool;
155
156    fn visit_intrinsic(&mut self, _kind: crate::types::IntrinsicKind) -> Self::Output {
157        false
158    }
159
160    fn visit_literal(&mut self, _value: &crate::LiteralValue) -> Self::Output {
161        false
162    }
163
164    fn visit_array(&mut self, _element_type: TypeId) -> Self::Output {
165        false // Arrays are mutable by default
166    }
167
168    fn visit_tuple(&mut self, _list_id: u32) -> Self::Output {
169        false // Tuples are mutable by default
170    }
171
172    fn visit_object_with_index(&mut self, shape_id: u32) -> Self::Output {
173        let shape = self.db.object_shape(ObjectShapeId(shape_id));
174        match self.kind {
175            IndexKind::String => shape.string_index.as_ref().is_some_and(|idx| idx.readonly),
176            IndexKind::Number => shape.number_index.as_ref().is_some_and(|idx| idx.readonly),
177        }
178    }
179
180    fn visit_union(&mut self, list_id: u32) -> Self::Output {
181        let types = self.db.type_list(crate::types::TypeListId(list_id));
182        // Union: any member being readonly makes it readonly
183        types.iter().any(|&t| self.visit_type(self.db, t))
184    }
185
186    fn visit_intersection(&mut self, list_id: u32) -> Self::Output {
187        let types = self.db.type_list(crate::types::TypeListId(list_id));
188        // Intersection: all must be readonly
189        types.iter().all(|&t| self.visit_type(self.db, t))
190    }
191
192    fn visit_readonly_type(&mut self, inner_type: TypeId) -> Self::Output {
193        self.visit_type(self.db, inner_type)
194    }
195
196    fn visit_lazy(&mut self, def_id: u32) -> Self::Output {
197        // Resolve lazy types (interfaces, classes, type aliases) before checking readonly
198        let resolved = crate::evaluation::evaluate::evaluate_type(self.db, TypeId(def_id));
199        self.visit_type(self.db, resolved)
200    }
201
202    fn default_output() -> Self::Output {
203        false
204    }
205}
206
207/// Visitor for collecting index signature information.
208struct IndexInfoCollector<'a> {
209    db: &'a dyn TypeDatabase,
210}
211
212impl<'a> TypeVisitor for IndexInfoCollector<'a> {
213    type Output = IndexInfo;
214
215    fn visit_intrinsic(&mut self, _kind: crate::types::IntrinsicKind) -> Self::Output {
216        IndexInfo {
217            string_index: None,
218            number_index: None,
219        }
220    }
221
222    fn visit_literal(&mut self, _value: &crate::LiteralValue) -> Self::Output {
223        IndexInfo {
224            string_index: None,
225            number_index: None,
226        }
227    }
228
229    fn visit_object_with_index(&mut self, shape_id: u32) -> Self::Output {
230        let shape = self.db.object_shape(ObjectShapeId(shape_id));
231        IndexInfo {
232            string_index: shape.string_index.clone(),
233            number_index: shape.number_index.clone(),
234        }
235    }
236
237    fn visit_array(&mut self, elem: TypeId) -> Self::Output {
238        IndexInfo {
239            string_index: None,
240            number_index: Some(IndexSignature {
241                key_type: TypeId::NUMBER,
242                value_type: elem,
243                readonly: false,
244            }),
245        }
246    }
247
248    fn visit_tuple(&mut self, _list_id: u32) -> Self::Output {
249        IndexInfo {
250            string_index: None,
251            number_index: Some(IndexSignature {
252                key_type: TypeId::NUMBER,
253                value_type: TypeId::UNKNOWN,
254                readonly: false,
255            }),
256        }
257    }
258
259    fn visit_readonly_type(&mut self, inner_type: TypeId) -> Self::Output {
260        let mut info = self.visit_type(self.db, inner_type);
261        // Mark all signatures as readonly
262        if let Some(idx) = &mut info.string_index {
263            idx.readonly = true;
264        }
265        if let Some(idx) = &mut info.number_index {
266            idx.readonly = true;
267        }
268        info
269    }
270
271    fn visit_union(&mut self, _list_id: u32) -> Self::Output {
272        // Complex logic, return empty for now
273        IndexInfo {
274            string_index: None,
275            number_index: None,
276        }
277    }
278
279    fn visit_intersection(&mut self, _list_id: u32) -> Self::Output {
280        IndexInfo {
281            string_index: None,
282            number_index: None,
283        }
284    }
285
286    fn visit_lazy(&mut self, def_id: u32) -> Self::Output {
287        // Resolve lazy types (interfaces, classes, type aliases) before collecting index info
288        let resolved = crate::evaluation::evaluate::evaluate_type(self.db, TypeId(def_id));
289        self.visit_type(self.db, resolved)
290    }
291
292    fn default_output() -> Self::Output {
293        IndexInfo {
294            string_index: None,
295            number_index: None,
296        }
297    }
298}
299
300/// Resolver for index signature queries on types.
301///
302/// This struct provides a unified interface for querying index signatures
303/// across different type representations (`ObjectWithIndex`, Union, etc.).
304pub struct IndexSignatureResolver<'a> {
305    db: &'a dyn TypeDatabase,
306}
307
308impl<'a> IndexSignatureResolver<'a> {
309    /// Create a new index signature resolver.
310    pub fn new(db: &'a dyn TypeDatabase) -> Self {
311        Self { db }
312    }
313
314    /// Resolve the string index signature type from an object type.
315    ///
316    /// Returns `Some(value_type)` if the object has a string index signature,
317    /// `None` otherwise.
318    ///
319    /// ## Examples
320    ///
321    /// - `{ [key: string]: number }` → `Some(TypeId::NUMBER)`
322    /// - `{ [key: string]: string }` → `Some(TypeId::STRING)`
323    /// - `{ a: number }` → `None`
324    pub fn resolve_string_index(&self, obj: TypeId) -> Option<TypeId> {
325        let mut visitor = StringIndexResolver { db: self.db };
326        visitor.visit_type(self.db, obj)
327    }
328
329    /// Resolve the numeric index signature type from an object type.
330    ///
331    /// Returns `Some(value_type)` if the object has a numeric index signature,
332    /// `None` otherwise.
333    ///
334    /// ## Examples
335    ///
336    /// - `{ [key: number]: string }` → `Some(TypeId::STRING)`
337    /// - `{ [key: number]: number }` → `Some(TypeId::NUMBER)`
338    /// - `{ a: number }` → `None`
339    ///
340    /// Note: Array and tuple types have implicit numeric index signatures.
341    pub fn resolve_number_index(&self, obj: TypeId) -> Option<TypeId> {
342        let mut visitor = NumberIndexResolver { db: self.db };
343        visitor.visit_type(self.db, obj)
344    }
345
346    /// Check if an index signature is readonly.
347    ///
348    /// ## Parameters
349    ///
350    /// - `obj`: The type to check
351    /// - `kind`: Which index signature to check (string or number)
352    ///
353    /// ## Returns
354    ///
355    /// `true` if the requested index signature is readonly, `false` otherwise.
356    ///
357    /// ## Examples
358    ///
359    /// - `{ readonly [x: string]: string }` with `IndexKind::String` → `true`
360    /// - `{ [x: string]: string }` with `IndexKind::String` → `false`
361    pub fn is_readonly(&self, obj: TypeId, kind: IndexKind) -> bool {
362        let mut visitor = ReadonlyChecker { db: self.db, kind };
363        visitor.visit_type(self.db, obj)
364    }
365
366    /// Get all index signatures from a type.
367    ///
368    /// Returns an `IndexInfo` struct containing both string and numeric
369    /// index signatures if present.
370    pub fn get_index_info(&self, obj: TypeId) -> IndexInfo {
371        let mut collector = IndexInfoCollector { db: self.db };
372        collector.visit_type(self.db, obj)
373    }
374
375    /// Check if a type has a specific index signature.
376    ///
377    /// ## Parameters
378    ///
379    /// - `obj`: The type to check
380    /// - `kind`: Which index signature to check for (string or number)
381    ///
382    /// ## Returns
383    ///
384    /// `true` if the type has the requested index signature, `false` otherwise.
385    pub fn has_index_signature(&self, obj: TypeId, kind: IndexKind) -> bool {
386        match kind {
387            IndexKind::String => self.resolve_string_index(obj).is_some(),
388            IndexKind::Number => self.resolve_number_index(obj).is_some(),
389        }
390    }
391
392    /// Check if a property name is a valid numeric index.
393    ///
394    /// ## Examples
395    ///
396    /// - `"0"` → `true`
397    /// - `"42"` → `true`
398    /// - `"foo"` → `false`
399    /// - `"-1"` → `false`
400    /// - `"NaN"` → `true`
401    /// - `"Infinity"` → `true`
402    pub fn is_numeric_index_name(&self, name: &str) -> bool {
403        if name.starts_with('-') && name != "-Infinity" {
404            return false;
405        }
406        utils::is_numeric_literal_name(name)
407    }
408}
409
410#[cfg(test)]
411#[path = "../../tests/index_signatures_tests.rs"]
412mod tests;