Skip to main content

tsz_solver/operations/
property_visitor.rs

1//! `TypeVisitor` implementation for `PropertyAccessEvaluator`.
2//!
3//! Contains the visitor dispatch that resolves property access for each
4//! type kind, plus impl helper methods for complex cases (objects, unions, etc.).
5
6use super::property::{PropertyAccessEvaluator, PropertyAccessResult};
7use crate::types::{IntrinsicKind, LiteralValue, ObjectShapeId, TupleListId, TypeId, TypeListId};
8use crate::visitor::TypeVisitor;
9use tsz_common::interner::Atom;
10
11// =============================================================================
12// TypeVisitor Implementation for PropertyAccessEvaluator
13// =============================================================================
14
15// Implement TypeVisitor for &PropertyAccessEvaluator to solve &mut self issue
16// This allows visitor methods to be called from &self methods while still
17// being able to mutate internal state via RefCells.
18impl<'a> TypeVisitor for &PropertyAccessEvaluator<'a> {
19    type Output = Option<PropertyAccessResult>;
20
21    fn visit_intrinsic(&mut self, kind: IntrinsicKind) -> Self::Output {
22        match kind {
23            IntrinsicKind::Any => Some(PropertyAccessResult::Success {
24                type_id: TypeId::ANY,
25                write_type: None,
26                from_index_signature: false,
27            }),
28            IntrinsicKind::Never => {
29                // Property access on never returns never (code is unreachable)
30                Some(PropertyAccessResult::Success {
31                    type_id: TypeId::NEVER,
32                    write_type: None,
33                    from_index_signature: false,
34                })
35            }
36            IntrinsicKind::Unknown => Some(PropertyAccessResult::IsUnknown),
37            IntrinsicKind::Void | IntrinsicKind::Null | IntrinsicKind::Undefined => {
38                let cause = if kind == IntrinsicKind::Void || kind == IntrinsicKind::Undefined {
39                    TypeId::UNDEFINED
40                } else {
41                    TypeId::NULL
42                };
43                Some(PropertyAccessResult::PossiblyNullOrUndefined {
44                    property_type: None,
45                    cause,
46                })
47            }
48            // Handle primitive intrinsic types by delegating to their boxed interfaces
49            IntrinsicKind::String => {
50                let prop_name = self.current_prop_name.borrow();
51                let prop_atom = self.current_prop_atom.borrow();
52                match (prop_name.as_deref(), prop_atom.as_ref()) {
53                    (Some(name), Some(&atom)) => Some(self.resolve_string_property(name, atom)),
54                    _ => None,
55                }
56            }
57            IntrinsicKind::Number => {
58                let prop_name = self.current_prop_name.borrow();
59                let prop_atom = self.current_prop_atom.borrow();
60                match (prop_name.as_deref(), prop_atom.as_ref()) {
61                    (Some(name), Some(&atom)) => Some(self.resolve_number_property(name, atom)),
62                    _ => None,
63                }
64            }
65            IntrinsicKind::Boolean => {
66                let prop_name = self.current_prop_name.borrow();
67                let prop_atom = self.current_prop_atom.borrow();
68                match (prop_name.as_deref(), prop_atom.as_ref()) {
69                    (Some(name), Some(&atom)) => Some(self.resolve_boolean_property(name, atom)),
70                    _ => None,
71                }
72            }
73            IntrinsicKind::Bigint => {
74                let prop_name = self.current_prop_name.borrow();
75                let prop_atom = self.current_prop_atom.borrow();
76                match (prop_name.as_deref(), prop_atom.as_ref()) {
77                    (Some(name), Some(&atom)) => Some(self.resolve_bigint_property(name, atom)),
78                    _ => None,
79                }
80            }
81            // Symbol intrinsic is handled separately (has special properties)
82            IntrinsicKind::Symbol => {
83                // Get the property name from context
84                let prop_name = self.current_prop_name.borrow();
85                let prop_atom = self.current_prop_atom.borrow();
86                match (prop_name.as_deref(), prop_atom.as_ref()) {
87                    (Some(name), Some(&atom)) => {
88                        Some(self.resolve_symbol_primitive_property(name, atom))
89                    }
90                    _ => None,
91                }
92            }
93            // Other intrinsics (Object, etc.) fall back to None
94            _ => None,
95        }
96    }
97
98    fn visit_literal(&mut self, value: &LiteralValue) -> Self::Output {
99        use crate::types::LiteralValue;
100
101        let prop_name = self.current_prop_name.borrow();
102        let prop_atom_opt = self.current_prop_atom.borrow();
103
104        let prop_name = prop_name.as_deref()?;
105        let prop_atom = match prop_atom_opt.as_ref() {
106            Some(&atom) => atom,
107            None => self.interner().intern_string(prop_name),
108        };
109
110        // Handle primitive literals by delegating to their boxed interface types
111        match value {
112            LiteralValue::String(_) => Some(self.resolve_string_property(prop_name, prop_atom)),
113            LiteralValue::Number(_) => Some(self.resolve_number_property(prop_name, prop_atom)),
114            LiteralValue::Boolean(_) => Some(self.resolve_boolean_property(prop_name, prop_atom)),
115            LiteralValue::BigInt(_) => Some(self.resolve_bigint_property(prop_name, prop_atom)),
116        }
117    }
118
119    fn visit_object(&mut self, shape_id: u32) -> Self::Output {
120        use crate::objects::index_signatures::{IndexKind, IndexSignatureResolver};
121
122        let prop_name = self.current_prop_name.borrow();
123        let prop_atom_opt = self.current_prop_atom.borrow();
124
125        let prop_name = prop_name.as_deref()?;
126        let prop_atom = match prop_atom_opt.as_ref() {
127            Some(&atom) => atom,
128            None => self.interner().intern_string(prop_name),
129        };
130
131        let shape = self.interner().object_shape(ObjectShapeId(shape_id));
132
133        // Check explicit properties first
134        if let Some(prop) =
135            self.lookup_object_property(ObjectShapeId(shape_id), &shape.properties, prop_atom)
136        {
137            let write = (prop.write_type != prop.type_id).then_some(prop.write_type);
138            return Some(PropertyAccessResult::Success {
139                type_id: self.optional_property_type(prop),
140                write_type: write,
141                from_index_signature: false,
142            });
143        }
144
145        // Check apparent members (toString, etc.)
146        if let Some(result) = self.resolve_object_member(prop_name, prop_atom) {
147            return Some(result);
148        }
149
150        // Check for index signatures (some Object types may have index signatures that aren't in ObjectWithIndex)
151        let resolver = IndexSignatureResolver::new(self.interner());
152
153        // Reconstruct obj_type from shape_id for index signature checking
154        let obj_type = self.interner().object_with_flags_and_symbol(
155            self.interner()
156                .object_shape(ObjectShapeId(shape_id))
157                .properties
158                .clone(),
159            self.interner().object_shape(ObjectShapeId(shape_id)).flags,
160            self.interner().object_shape(ObjectShapeId(shape_id)).symbol,
161        );
162
163        // Try string index signature first (most common)
164        if resolver.has_index_signature(obj_type, IndexKind::String)
165            && let Some(value_type) = resolver.resolve_string_index(obj_type)
166        {
167            return Some(PropertyAccessResult::Success {
168                type_id: self.add_undefined_if_unchecked(value_type),
169                write_type: None,
170                from_index_signature: true,
171            });
172        }
173
174        // Try numeric index signature if property name looks numeric
175        if resolver.is_numeric_index_name(prop_name)
176            && let Some(value_type) = resolver.resolve_number_index(obj_type)
177        {
178            return Some(PropertyAccessResult::Success {
179                type_id: self.add_undefined_if_unchecked(value_type),
180                write_type: None,
181                from_index_signature: true,
182            });
183        }
184
185        Some(PropertyAccessResult::PropertyNotFound {
186            type_id: obj_type,
187            property_name: prop_atom,
188        })
189    }
190
191    fn visit_object_with_index(&mut self, shape_id: u32) -> Self::Output {
192        use crate::objects::index_signatures::IndexSignatureResolver;
193
194        let prop_name = self.current_prop_name.borrow();
195        let prop_atom_opt = self.current_prop_atom.borrow();
196
197        let prop_name = prop_name.as_deref()?;
198        let prop_atom = match prop_atom_opt.as_ref() {
199            Some(&atom) => atom,
200            None => self.interner().intern_string(prop_name),
201        };
202
203        let shape = self.interner().object_shape(ObjectShapeId(shape_id));
204
205        // Check explicit properties first
206        if let Some(prop) =
207            self.lookup_object_property(ObjectShapeId(shape_id), &shape.properties, prop_atom)
208        {
209            let write = (prop.write_type != prop.type_id).then_some(prop.write_type);
210            return Some(PropertyAccessResult::Success {
211                type_id: self.optional_property_type(prop),
212                write_type: write,
213                from_index_signature: false,
214            });
215        }
216
217        // Check apparent members (toString, etc.)
218        if let Some(result) = self.resolve_object_member(prop_name, prop_atom) {
219            return Some(result);
220        }
221
222        // Check string index signature
223        if let Some(ref idx) = shape.string_index {
224            return Some(PropertyAccessResult::Success {
225                type_id: self.add_undefined_if_unchecked(idx.value_type),
226                write_type: None,
227                from_index_signature: true,
228            });
229        }
230
231        // Check numeric index signature if property name looks numeric
232        let resolver = IndexSignatureResolver::new(self.interner());
233        if resolver.is_numeric_index_name(prop_name)
234            && let Some(ref idx) = shape.number_index
235        {
236            return Some(PropertyAccessResult::Success {
237                type_id: self.add_undefined_if_unchecked(idx.value_type),
238                write_type: None,
239                from_index_signature: true,
240            });
241        }
242
243        // Reconstruct obj_type for PropertyNotFound result
244        let obj_type = self.interner().object_with_index(
245            self.interner()
246                .object_shape(ObjectShapeId(shape_id))
247                .as_ref()
248                .clone(),
249        );
250
251        Some(PropertyAccessResult::PropertyNotFound {
252            type_id: obj_type,
253            property_name: prop_atom,
254        })
255    }
256
257    fn visit_array(&mut self, element_type: TypeId) -> Self::Output {
258        let prop_name = self.current_prop_name.borrow();
259        let prop_atom_opt = self.current_prop_atom.borrow();
260
261        let prop_name = prop_name.as_deref()?;
262        let prop_atom = match prop_atom_opt.as_ref() {
263            Some(&atom) => atom,
264            None => self.interner().intern_string(prop_name),
265        };
266
267        // Reconstruct obj_type for resolve_array_property
268        let obj_type = self.interner().array(element_type);
269        Some(self.resolve_array_property(obj_type, prop_name, prop_atom))
270    }
271
272    fn visit_tuple(&mut self, list_id: u32) -> Self::Output {
273        let prop_name = self.current_prop_name.borrow();
274        let prop_atom_opt = self.current_prop_atom.borrow();
275
276        let prop_name = prop_name.as_deref()?;
277        let prop_atom = match prop_atom_opt.as_ref() {
278            Some(&atom) => atom,
279            None => self.interner().intern_string(prop_name),
280        };
281
282        // Reconstruct obj_type for resolve_array_property
283        let obj_type = self
284            .interner()
285            .tuple(self.interner().tuple_list(TupleListId(list_id)).to_vec());
286        Some(self.resolve_array_property(obj_type, prop_name, prop_atom))
287    }
288
289    fn visit_template_literal(&mut self, _template_id: u32) -> Self::Output {
290        // Template literals are string-like for property access
291        // They support the same properties as the String interface
292        let prop_name = self.current_prop_name.borrow();
293        let prop_atom_opt = self.current_prop_atom.borrow();
294
295        let prop_name = prop_name.as_deref()?;
296        let prop_atom = match prop_atom_opt.as_ref() {
297            Some(&atom) => atom,
298            None => self.interner().intern_string(prop_name),
299        };
300
301        Some(self.resolve_string_property(prop_name, prop_atom))
302    }
303
304    fn visit_string_intrinsic(
305        &mut self,
306        _kind: crate::types::StringIntrinsicKind,
307        _type_arg: TypeId,
308    ) -> Self::Output {
309        // String intrinsics (Uppercase<T>, Lowercase<T>, Capitalize<T>, etc.)
310        // are string-like for property access
311        let prop_name = self.current_prop_name.borrow();
312        let prop_atom_opt = self.current_prop_atom.borrow();
313
314        let prop_name = prop_name.as_deref()?;
315        let prop_atom = match prop_atom_opt.as_ref() {
316            Some(&atom) => atom,
317            None => self.interner().intern_string(prop_name),
318        };
319
320        Some(self.resolve_string_property(prop_name, prop_atom))
321    }
322
323    fn visit_union(&mut self, list_id: u32) -> Self::Output {
324        let prop_name = self.current_prop_name.borrow();
325        let prop_atom_opt = self.current_prop_atom.borrow();
326
327        let prop_name = prop_name.as_deref()?;
328
329        self.visit_union_impl(list_id, prop_name, prop_atom_opt.as_ref().copied())
330    }
331
332    fn default_output() -> Self::Output {
333        None
334    }
335}
336
337impl<'a> PropertyAccessEvaluator<'a> {
338    // Helper methods to call visitor logic from &self context
339    // These contain the actual implementation that the TypeVisitor trait methods delegate to
340
341    pub(crate) fn visit_object_impl(
342        &self,
343        shape_id: u32,
344        prop_name: &str,
345        prop_atom: Option<Atom>,
346    ) -> Option<PropertyAccessResult> {
347        use crate::objects::index_signatures::{IndexKind, IndexSignatureResolver};
348
349        let prop_atom = match prop_atom {
350            Some(atom) => atom,
351            None => self.interner().intern_string(prop_name),
352        };
353
354        let shape = self.interner().object_shape(ObjectShapeId(shape_id));
355
356        // Check explicit properties first
357        if let Some(prop) =
358            self.lookup_object_property(ObjectShapeId(shape_id), &shape.properties, prop_atom)
359        {
360            let write = (prop.write_type != prop.type_id).then_some(prop.write_type);
361            return Some(PropertyAccessResult::Success {
362                type_id: self.optional_property_type(prop),
363                write_type: write,
364                from_index_signature: false,
365            });
366        }
367
368        // Check apparent members (toString, etc.)
369        if let Some(result) = self.resolve_object_member(prop_name, prop_atom) {
370            return Some(result);
371        }
372
373        // Check for index signatures (some Object types may have index signatures that aren't in ObjectWithIndex)
374        let resolver = IndexSignatureResolver::new(self.interner());
375
376        // Reconstruct obj_type from shape_id for index signature checking
377        let obj_type = self.interner().object_with_flags_and_symbol(
378            self.interner()
379                .object_shape(ObjectShapeId(shape_id))
380                .properties
381                .clone(),
382            self.interner().object_shape(ObjectShapeId(shape_id)).flags,
383            self.interner().object_shape(ObjectShapeId(shape_id)).symbol,
384        );
385
386        // Try string index signature first (most common)
387        if resolver.has_index_signature(obj_type, IndexKind::String)
388            && let Some(value_type) = resolver.resolve_string_index(obj_type)
389        {
390            return Some(PropertyAccessResult::Success {
391                type_id: self.add_undefined_if_unchecked(value_type),
392                write_type: None,
393                from_index_signature: true,
394            });
395        }
396
397        // Try numeric index signature if property name looks numeric
398        if resolver.is_numeric_index_name(prop_name)
399            && let Some(value_type) = resolver.resolve_number_index(obj_type)
400        {
401            return Some(PropertyAccessResult::Success {
402                type_id: self.add_undefined_if_unchecked(value_type),
403                write_type: None,
404                from_index_signature: true,
405            });
406        }
407
408        Some(PropertyAccessResult::PropertyNotFound {
409            type_id: obj_type,
410            property_name: prop_atom,
411        })
412    }
413
414    pub(crate) fn visit_object_with_index_impl(
415        &self,
416        shape_id: u32,
417        prop_name: &str,
418        prop_atom: Option<Atom>,
419    ) -> Option<PropertyAccessResult> {
420        use crate::objects::index_signatures::IndexSignatureResolver;
421
422        let prop_atom = match prop_atom {
423            Some(atom) => atom,
424            None => self.interner().intern_string(prop_name),
425        };
426
427        let shape = self.interner().object_shape(ObjectShapeId(shape_id));
428
429        // Check explicit properties first
430        if let Some(prop) =
431            self.lookup_object_property(ObjectShapeId(shape_id), &shape.properties, prop_atom)
432        {
433            let write = (prop.write_type != prop.type_id).then_some(prop.write_type);
434            return Some(PropertyAccessResult::Success {
435                type_id: self.optional_property_type(prop),
436                write_type: write,
437                from_index_signature: false,
438            });
439        }
440
441        // Check apparent members (toString, etc.)
442        if let Some(result) = self.resolve_object_member(prop_name, prop_atom) {
443            return Some(result);
444        }
445
446        // Check string index signature
447        if let Some(ref idx) = shape.string_index {
448            return Some(PropertyAccessResult::Success {
449                type_id: self.add_undefined_if_unchecked(idx.value_type),
450                write_type: None,
451                from_index_signature: true,
452            });
453        }
454
455        // Check numeric index signature if property name looks numeric
456        let resolver = IndexSignatureResolver::new(self.interner());
457        if resolver.is_numeric_index_name(prop_name)
458            && let Some(ref idx) = shape.number_index
459        {
460            return Some(PropertyAccessResult::Success {
461                type_id: self.add_undefined_if_unchecked(idx.value_type),
462                write_type: None,
463                from_index_signature: true,
464            });
465        }
466
467        // Reconstruct obj_type for PropertyNotFound result
468        let obj_type = self.interner().object_with_index((*shape).clone());
469
470        Some(PropertyAccessResult::PropertyNotFound {
471            type_id: obj_type,
472            property_name: prop_atom,
473        })
474    }
475
476    pub(crate) fn visit_array_impl(
477        &self,
478        obj_type: TypeId,
479        prop_name: &str,
480        prop_atom: Option<Atom>,
481    ) -> Option<PropertyAccessResult> {
482        let prop_atom = match prop_atom {
483            Some(atom) => atom,
484            None => self.interner().intern_string(prop_name),
485        };
486        Some(self.resolve_array_property(obj_type, prop_name, prop_atom))
487    }
488
489    pub(crate) fn visit_union_impl(
490        &self,
491        list_id: u32,
492        prop_name: &str,
493        prop_atom: Option<Atom>,
494    ) -> Option<PropertyAccessResult> {
495        use crate::objects::index_signatures::{IndexKind, IndexSignatureResolver};
496
497        let members = self.interner().type_list(crate::types::TypeListId(list_id));
498
499        // Fast-path: if ANY member is any, result is any
500        if members.contains(&TypeId::ANY) {
501            return Some(PropertyAccessResult::Success {
502                type_id: TypeId::ANY,
503                write_type: None,
504                from_index_signature: false,
505            });
506        }
507
508        // Fast-path: if ANY member is error, result is error
509        if members.contains(&TypeId::ERROR) {
510            return Some(PropertyAccessResult::Success {
511                type_id: TypeId::ERROR,
512                write_type: None,
513                from_index_signature: false,
514            });
515        }
516
517        // Filter out UNKNOWN members - they shouldn't cause the entire union to be unknown
518        // Only return IsUnknown if ALL members are UNKNOWN
519        let non_unknown_members: Vec<_> = members
520            .iter()
521            .filter(|&&t| t != TypeId::UNKNOWN)
522            .copied()
523            .collect();
524
525        if non_unknown_members.is_empty() {
526            // All members are UNKNOWN
527            return Some(PropertyAccessResult::IsUnknown);
528        }
529
530        // Reconstructing the union can be expensive for large unions. Delay it
531        // until we actually need it for an error/index-signature fallback path.
532        let mut obj_type_cache: Option<TypeId> = None;
533        let mut obj_type_for_error = || {
534            *obj_type_cache.get_or_insert_with(|| {
535                self.interner()
536                    .union(self.interner().type_list(TypeListId(list_id)).to_vec())
537            })
538        };
539
540        let prop_atom = match prop_atom {
541            Some(atom) => atom,
542            None => self.interner().intern_string(prop_name),
543        };
544
545        // Property access on union: partition into nullable and non-nullable members
546        let mut valid_results = Vec::new();
547        let mut valid_write_results = Vec::new();
548        let mut any_has_divergent_write_type = false;
549        let mut nullable_causes = Vec::new();
550        let mut any_from_index = false; // Track if any member used index signature
551
552        for &member in &non_unknown_members {
553            // Check for null/undefined directly
554            if member.is_nullable() {
555                let cause = if member == TypeId::VOID {
556                    TypeId::UNDEFINED
557                } else {
558                    member
559                };
560                nullable_causes.push(cause);
561                continue;
562            }
563
564            match self.resolve_property_access_inner(member, prop_name, Some(prop_atom)) {
565                PropertyAccessResult::Success {
566                    type_id,
567                    write_type,
568                    from_index_signature,
569                } => {
570                    valid_results.push(type_id);
571                    if let Some(wt) = write_type {
572                        valid_write_results.push(wt);
573                        any_has_divergent_write_type = true;
574                    } else {
575                        valid_write_results.push(type_id);
576                    }
577                    if from_index_signature {
578                        any_from_index = true; // Propagate: if ANY member uses index, flag it
579                    }
580                }
581                PropertyAccessResult::PossiblyNullOrUndefined {
582                    property_type,
583                    cause,
584                } => {
585                    if let Some(t) = property_type {
586                        valid_results.push(t);
587                        valid_write_results.push(t);
588                    }
589                    nullable_causes.push(cause);
590                }
591                // PropertyNotFound: if ANY member is missing the property, the property does not exist on the Union
592                PropertyAccessResult::PropertyNotFound { .. } => {
593                    return Some(PropertyAccessResult::PropertyNotFound {
594                        type_id: obj_type_for_error(),
595                        property_name: prop_atom,
596                    });
597                }
598                // IsUnknown: if any member is unknown, we cannot safely access the property
599                PropertyAccessResult::IsUnknown => {
600                    return Some(PropertyAccessResult::IsUnknown);
601                }
602            }
603        }
604
605        // If no non-nullable members had the property, it's a PropertyNotFound error
606        if valid_results.is_empty() && nullable_causes.is_empty() {
607            // Before giving up, check union-level index signatures
608            let resolver = IndexSignatureResolver::new(self.interner());
609            let obj_type = obj_type_for_error();
610
611            if resolver.has_index_signature(obj_type, IndexKind::String)
612                && let Some(value_type) = resolver.resolve_string_index(obj_type)
613            {
614                return Some(PropertyAccessResult::Success {
615                    type_id: self.add_undefined_if_unchecked(value_type),
616                    write_type: None,
617                    from_index_signature: true,
618                });
619            }
620
621            if resolver.is_numeric_index_name(prop_name)
622                && let Some(value_type) = resolver.resolve_number_index(obj_type)
623            {
624                return Some(PropertyAccessResult::Success {
625                    type_id: self.add_undefined_if_unchecked(value_type),
626                    write_type: None,
627                    from_index_signature: true,
628                });
629            }
630
631            return Some(PropertyAccessResult::PropertyNotFound {
632                type_id: obj_type,
633                property_name: prop_atom,
634            });
635        }
636
637        // If there are nullable causes, return PossiblyNullOrUndefined
638        if !nullable_causes.is_empty() {
639            let cause = if nullable_causes.len() == 1 {
640                nullable_causes[0]
641            } else {
642                self.interner().union(nullable_causes)
643            };
644
645            let mut property_type = if valid_results.is_empty() {
646                None
647            } else if valid_results.len() == 1 {
648                Some(valid_results[0])
649            } else {
650                Some(self.interner().union(valid_results))
651            };
652
653            if any_from_index
654                && self.no_unchecked_indexed_access
655                && let Some(t) = property_type
656            {
657                property_type = Some(self.add_undefined_if_unchecked(t));
658            }
659
660            return Some(PropertyAccessResult::PossiblyNullOrUndefined {
661                property_type,
662                cause,
663            });
664        }
665
666        let mut type_id = self.interner().union(valid_results);
667        if any_from_index && self.no_unchecked_indexed_access {
668            type_id = self.add_undefined_if_unchecked(type_id);
669        }
670
671        let write_type = if any_has_divergent_write_type {
672            let mut wt = self.interner().union(valid_write_results);
673            if any_from_index && self.no_unchecked_indexed_access {
674                wt = self.add_undefined_if_unchecked(wt);
675            }
676            if wt != type_id { Some(wt) } else { None }
677        } else {
678            None
679        };
680
681        // Union of all result types
682        Some(PropertyAccessResult::Success {
683            type_id,
684            write_type,
685            from_index_signature: any_from_index, // Contagious across union members
686        })
687    }
688}