Skip to main content

tsz_solver/objects/
collect.rs

1//! Property collection and merging for intersection types.
2//!
3//! This module provides utilities for collecting properties from intersection types
4//! while handling Lazy/Ref resolution and avoiding infinite recursion.
5
6use crate::relations::subtype::TypeResolver;
7#[cfg(test)]
8use crate::types::*;
9use crate::types::{
10    IndexSignature, IntrinsicKind, ObjectShape, PropertyInfo, TypeData, TypeId, TypeListId,
11    Visibility,
12};
13use rustc_hash::{FxHashMap, FxHashSet};
14use tsz_common::interner::Atom;
15
16// Import TypeDatabase trait
17use crate::caches::db::TypeDatabase;
18
19/// Merge two visibility levels, returning the more restrictive one.
20///
21/// Ordering: Private > Protected > Public
22const fn merge_visibility(a: Visibility, b: Visibility) -> Visibility {
23    match (a, b) {
24        (Visibility::Private, _) | (_, Visibility::Private) => Visibility::Private,
25        (Visibility::Protected, _) | (_, Visibility::Protected) => Visibility::Protected,
26        (Visibility::Public, Visibility::Public) => Visibility::Public,
27    }
28}
29
30/// Result of collecting properties from an intersection type.
31#[derive(Debug, Clone, PartialEq)]
32pub enum PropertyCollectionResult {
33    /// The intersection contains `any`, making the entire type `any`
34    Any,
35    /// The intersection contains only non-object types (never, unknown, primitives, etc.)
36    NonObject,
37    /// The intersection contains object properties
38    Properties {
39        properties: Vec<PropertyInfo>,
40        string_index: Option<IndexSignature>,
41        number_index: Option<IndexSignature>,
42    },
43}
44
45/// Collect properties from an intersection type, recursively merging all members.
46///
47/// This function handles:
48/// - Recursive traversal of intersection members
49/// - Lazy/Ref type resolution
50/// - Property type intersection (using raw intersection to avoid recursion)
51/// - Optionality merging (required wins)
52/// - Readonly merging (readonly is cumulative)
53/// - Index signature merging
54///
55/// # Arguments
56/// * `type_id` - The type to collect properties from (may be an intersection)
57/// * `interner` - The type interner for type operations
58/// * `resolver` - Type resolver for handling Lazy/Ref types
59///
60/// # Returns
61/// A `PropertyCollectionResult` indicating whether the result is `Any`, non-object,
62/// or contains actual properties.
63///
64/// # Important
65/// - Call signatures are NOT collected (this is for properties only)
66/// - Mapped types are NOT handled (input should be pre-lowered/evaluated)
67/// - `any & T` always returns `Any` (commutative)
68pub fn collect_properties<R>(
69    type_id: TypeId,
70    interner: &dyn TypeDatabase,
71    resolver: &R,
72) -> PropertyCollectionResult
73where
74    R: TypeResolver,
75{
76    let mut collector = PropertyCollector {
77        interner,
78        resolver,
79        properties: Vec::new(),
80        prop_index: FxHashMap::default(),
81        string_index: None,
82        number_index: None,
83        seen: FxHashSet::default(),
84        found_any: false,
85    };
86    collector.collect(type_id);
87
88    // If we encountered Any at any point, the result is Any (commutative)
89    if collector.found_any {
90        return PropertyCollectionResult::Any;
91    }
92
93    // If no properties were collected, return NonObject
94    if collector.properties.is_empty()
95        && collector.string_index.is_none()
96        && collector.number_index.is_none()
97    {
98        return PropertyCollectionResult::NonObject;
99    }
100
101    // Sort properties by name to maintain interner invariants
102    collector.properties.sort_by_key(|p| p.name.0);
103
104    PropertyCollectionResult::Properties {
105        properties: collector.properties,
106        string_index: collector.string_index,
107        number_index: collector.number_index,
108    }
109}
110
111/// Helper function to resolve Lazy and Ref types
112fn resolve_type<R>(type_id: TypeId, interner: &dyn TypeDatabase, resolver: &R) -> TypeId
113where
114    R: TypeResolver,
115{
116    use crate::visitor::{lazy_def_id, ref_symbol};
117
118    // Handle DefId-based Lazy types (new API)
119    if let Some(def_id) = lazy_def_id(interner, type_id) {
120        return resolver.resolve_lazy(def_id, interner).unwrap_or(type_id);
121    }
122
123    // Handle legacy SymbolRef-based types (old API)
124    if let Some(symbol) = ref_symbol(interner, type_id) {
125        resolver
126            .resolve_symbol_ref(symbol, interner)
127            .unwrap_or(type_id)
128    } else {
129        type_id
130    }
131}
132
133/// Property collector for intersection types.
134///
135/// Recursively walks intersection members and collects all properties,
136/// merging properties with the same name using intersection types.
137struct PropertyCollector<'a, R> {
138    interner: &'a dyn TypeDatabase,
139    resolver: &'a R,
140    properties: Vec<PropertyInfo>,
141    /// Maps property name (Atom) to index in `properties` for O(1) lookup during merge
142    prop_index: FxHashMap<Atom, usize>,
143    string_index: Option<IndexSignature>,
144    number_index: Option<IndexSignature>,
145    /// Prevent infinite recursion for circular intersections like: type T = { a: number } & T
146    seen: FxHashSet<TypeId>,
147    /// Track if we encountered Any (makes the whole result Any, commutative)
148    found_any: bool,
149}
150
151impl<'a, R: TypeResolver> PropertyCollector<'a, R> {
152    fn collect(&mut self, type_id: TypeId) {
153        // Prevent infinite recursion
154        if !self.seen.insert(type_id) {
155            return;
156        }
157
158        // 1. Resolve Lazy/Ref
159        let resolved = resolve_type(type_id, self.interner, self.resolver);
160
161        // 2. Handle different type variants
162        match self.interner.lookup(resolved) {
163            Some(TypeData::Intersection(members_id)) => {
164                // Recursively collect from all intersection members
165                for &member in self.interner.type_list(members_id).iter() {
166                    self.collect(member);
167                }
168            }
169            Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
170                let shape = self.interner.object_shape(shape_id);
171                self.merge_shape(&shape);
172            }
173            // Any type in intersection makes everything Any (commutative)
174            Some(TypeData::Intrinsic(IntrinsicKind::Any)) => {
175                self.found_any = true;
176            }
177            // Type parameter: collect properties from its constraint
178            Some(TypeData::TypeParameter(info)) => {
179                if let Some(constraint) = info.constraint {
180                    self.collect(constraint);
181                }
182            }
183            // Union: collect common properties (present in ALL members)
184            Some(TypeData::Union(members_id)) => {
185                self.collect_union_common(members_id);
186            }
187            // Never in intersection makes the whole thing Never
188            // This is handled by the caller, not here
189            _ => {
190                // Not an object or intersection - ignore (call signatures, primitives, etc.)
191            }
192        }
193    }
194
195    /// Collect common properties from all union members.
196    /// Only properties present in ALL members are included.
197    /// Property types become the union of the individual types.
198    fn collect_union_common(&mut self, members_id: TypeListId) {
199        let member_list = self.interner.type_list(members_id);
200        if member_list.is_empty() {
201            return;
202        }
203
204        // Collect properties from each union member using sub-collectors
205        let mut member_props: Vec<PropertyCollectionResult> = Vec::new();
206        for &member in member_list.iter() {
207            let result = collect_properties(member, self.interner, self.resolver);
208            member_props.push(result);
209        }
210
211        // If any member is Any, the whole union is Any
212        if member_props
213            .iter()
214            .any(|r| matches!(r, PropertyCollectionResult::Any))
215        {
216            self.found_any = true;
217            return;
218        }
219
220        // Collect property names present in ALL members
221        // Start with first member's property names, intersect with rest
222        let first = match &member_props[0] {
223            PropertyCollectionResult::Properties { properties, .. } => properties,
224            _ => return, // First member has no properties
225        };
226
227        // For each property in the first member, check if it's in all others
228        for prop in first {
229            let mut present_in_all = true;
230            let mut type_ids = vec![prop.type_id];
231            let mut all_optional = prop.optional;
232            let mut any_readonly = prop.readonly;
233
234            for member_result in member_props.iter().skip(1) {
235                match member_result {
236                    PropertyCollectionResult::Properties { properties, .. } => {
237                        if let Some(other_prop) = PropertyInfo::find_in_slice(properties, prop.name)
238                        {
239                            type_ids.push(other_prop.type_id);
240                            all_optional = all_optional && other_prop.optional;
241                            any_readonly = any_readonly || other_prop.readonly;
242                        } else {
243                            present_in_all = false;
244                            break;
245                        }
246                    }
247                    _ => {
248                        present_in_all = false;
249                        break;
250                    }
251                }
252            }
253
254            if present_in_all {
255                // Create union type for the property
256                let union_type = if type_ids.len() == 1 {
257                    type_ids[0]
258                } else {
259                    self.interner.union(type_ids)
260                };
261
262                // Merge into our properties
263                if let Some(&idx) = self.prop_index.get(&prop.name) {
264                    let existing = &mut self.properties[idx];
265                    existing.type_id = self
266                        .interner
267                        .intersect_types_raw2(existing.type_id, union_type);
268                    existing.optional = existing.optional && all_optional;
269                    existing.readonly = existing.readonly || any_readonly;
270                } else {
271                    let new_idx = self.properties.len();
272                    self.prop_index.insert(prop.name, new_idx);
273                    self.properties.push(PropertyInfo {
274                        name: prop.name,
275                        type_id: union_type,
276                        write_type: union_type,
277                        optional: all_optional,
278                        readonly: any_readonly,
279                        visibility: prop.visibility,
280                        is_method: prop.is_method,
281                        parent_id: prop.parent_id,
282                    });
283                }
284            }
285        }
286    }
287
288    fn merge_shape(&mut self, shape: &ObjectShape) {
289        // Merge properties using HashMap index for O(1) lookup
290        for prop in &shape.properties {
291            if let Some(&idx) = self.prop_index.get(&prop.name) {
292                let existing = &mut self.properties[idx];
293                // TS Rule: Intersect types (using raw to avoid recursion)
294                existing.type_id = self
295                    .interner
296                    .intersect_types_raw2(existing.type_id, prop.type_id);
297                existing.write_type = self
298                    .interner
299                    .intersect_types_raw2(existing.write_type, prop.write_type);
300                // TS Rule: Optional if ALL are optional (required wins)
301                existing.optional = existing.optional && prop.optional;
302                // TS Rule: Readonly if ANY is readonly (readonly is cumulative)
303                existing.readonly = existing.readonly || prop.readonly;
304                // Merge visibility: use the more restrictive one (private > protected > public)
305                existing.visibility = merge_visibility(existing.visibility, prop.visibility);
306                // is_method: if one is a method, treat as property (more general)
307                existing.is_method = existing.is_method && prop.is_method;
308            } else {
309                let new_idx = self.properties.len();
310                self.prop_index.insert(prop.name, new_idx);
311                self.properties.push(prop.clone());
312            }
313        }
314
315        // Merge string index signature
316        if let Some(ref idx) = shape.string_index {
317            if let Some(existing) = &mut self.string_index {
318                // Intersect value types
319                existing.value_type = self
320                    .interner
321                    .intersect_types_raw2(existing.value_type, idx.value_type);
322                // Readonly if ANY is readonly
323                existing.readonly = existing.readonly || idx.readonly;
324            } else {
325                self.string_index = Some(idx.clone());
326            }
327        }
328
329        // Merge number index signature
330        if let Some(ref idx) = shape.number_index {
331            if let Some(existing) = &mut self.number_index {
332                // Intersect value types
333                existing.value_type = self
334                    .interner
335                    .intersect_types_raw2(existing.value_type, idx.value_type);
336                // Readonly if ANY is readonly
337                existing.readonly = existing.readonly || idx.readonly;
338            } else {
339                self.number_index = Some(idx.clone());
340            }
341        }
342    }
343}
344
345#[cfg(test)]
346#[path = "../../tests/objects_tests.rs"]
347mod tests;