Skip to main content

tsz_solver/narrowing/
property.rs

1//! Property-based type narrowing.
2//!
3//! This module contains narrowing methods for:
4//! - `in` operator narrowing (property presence check)
5//! - Property type lookup for narrowing
6//! - Object-like type detection for instanceof support
7
8use super::NarrowingContext;
9use crate::types::{ObjectShapeId, PropertyInfo, TypeId, Visibility};
10use crate::visitor::{
11    intersection_list_id, object_shape_id, object_with_index_shape_id, type_param_info,
12    union_list_id,
13};
14use tracing::{Level, span, trace};
15use tsz_common::interner::Atom;
16
17impl<'a> NarrowingContext<'a> {
18    /// Check if a type is object-like (has object structure)
19    ///
20    /// This is used to determine if two types can form an intersection
21    /// for instanceof narrowing when they're not directly assignable.
22    pub(crate) fn are_object_like(&self, type_id: TypeId) -> bool {
23        use crate::types::TypeData;
24
25        match self.db.lookup(type_id) {
26            Some(
27                TypeData::Object(_)
28                | TypeData::ObjectWithIndex(_)
29                | TypeData::Function(_)
30                | TypeData::Callable(_),
31            ) => true,
32
33            // Interface and class types (which are object-like)
34            Some(TypeData::Application(_)) => {
35                // Check if the application type has construct signatures or object structure
36                use crate::type_queries::{InstanceTypeKind, classify_for_instance_type};
37
38                matches!(
39                    classify_for_instance_type(self.db, type_id),
40                    InstanceTypeKind::Callable(_) | InstanceTypeKind::Function(_)
41                )
42            }
43
44            // Type parameters - check their constraint
45            Some(TypeData::TypeParameter(info)) => {
46                // For instanceof, generics with object constraints are treated as object-like
47                // This allows intersection narrowing for cases like: T & MyClass
48                info.constraint.is_none_or(|c| self.are_object_like(c))
49            }
50
51            // Intersection of object types
52            Some(TypeData::Intersection(members)) => {
53                let members = self.db.type_list(members);
54                members.iter().any(|&member| self.are_object_like(member))
55            }
56
57            _ => false,
58        }
59    }
60
61    /// Narrow a type based on an `in` operator check.
62    ///
63    /// Example: `"a" in x` narrows `A | B` to include only types that have property `a`
64    pub fn narrow_by_property_presence(
65        &self,
66        source_type: TypeId,
67        property_name: Atom,
68        present: bool,
69    ) -> TypeId {
70        let _span = span!(
71            Level::TRACE,
72            "narrow_by_property_presence",
73            source_type = source_type.0,
74            ?property_name,
75            present
76        )
77        .entered();
78
79        // Handle special cases
80        if source_type == TypeId::ANY {
81            trace!("Source type is ANY, returning unchanged");
82            return TypeId::ANY;
83        }
84
85        if source_type == TypeId::NEVER {
86            trace!("Source type is NEVER, returning unchanged");
87            return TypeId::NEVER;
88        }
89
90        if source_type == TypeId::UNKNOWN {
91            if !present {
92                // False branch: property is not present. Since unknown could be anything,
93                // it remains unknown in the false branch.
94                trace!("UNKNOWN in false branch for in operator, returning UNKNOWN");
95                return TypeId::UNKNOWN;
96            }
97
98            // For unknown, narrow to object & { [prop]: unknown }
99            // This matches TypeScript's behavior where `in` check on unknown
100            // narrows to object type with the property
101            let prop_type = TypeId::UNKNOWN;
102            let required_prop = PropertyInfo {
103                name: property_name,
104                type_id: prop_type,
105                write_type: prop_type,
106                optional: false, // Property becomes required after `in` check
107                readonly: false,
108                is_method: false,
109                visibility: Visibility::Public,
110                parent_id: None,
111            };
112            let filter_obj = self.db.object(vec![required_prop]);
113            let narrowed = self.db.intersection2(TypeId::OBJECT, filter_obj);
114            trace!("Narrowing unknown to object & property = {}", narrowed.0);
115            return narrowed;
116        }
117
118        // Handle type parameters: narrow the constraint and intersect if changed
119        if let Some(type_param_info) = type_param_info(self.db, source_type) {
120            if let Some(constraint) = type_param_info.constraint
121                && constraint != source_type
122            {
123                let narrowed_constraint =
124                    self.narrow_by_property_presence(constraint, property_name, present);
125                if narrowed_constraint != constraint {
126                    trace!(
127                        "Type parameter constraint narrowed from {} to {}, creating intersection",
128                        constraint.0, narrowed_constraint.0
129                    );
130                    return self.db.intersection2(source_type, narrowed_constraint);
131                }
132            }
133            // Type parameter with no constraint or unchanged constraint
134            trace!("Type parameter unchanged, returning source");
135            return source_type;
136        }
137
138        // If source is a union, filter members based on property presence
139        if let Some(members_id) = union_list_id(self.db, source_type) {
140            let members = self.db.type_list(members_id);
141            trace!(
142                "Checking property {} in union with {} members",
143                self.db.resolve_atom_ref(property_name),
144                members.len()
145            );
146
147            let matching: Vec<TypeId> = members
148                .iter()
149                .map(|&member| {
150                    // CRITICAL: Resolve Lazy types for each member
151                    let resolved_member = self.resolve_type(member);
152
153                    let has_property = self.type_has_property(resolved_member, property_name);
154                    if present {
155                        // Positive: "prop" in member
156                        if has_property {
157                            // Property exists: Keep the member as-is
158                            // CRITICAL: For union narrowing, we don't modify the member type
159                            // We just filter to keep only members that have the property
160                            member
161                        } else {
162                            // Property not found: Exclude member (return NEVER)
163                            // Per TypeScript: "prop in x" being true means x MUST have the property
164                            // If x doesn't have it (and no index signature), narrow to never
165                            TypeId::NEVER
166                        }
167                    } else {
168                        // Negative: !("prop" in member)
169                        // Exclude member ONLY if property is required
170                        if self.is_property_required(resolved_member, property_name) {
171                            return TypeId::NEVER;
172                        }
173                        // Keep member (no required property found, or property is optional)
174                        member
175                    }
176                })
177                .collect();
178
179            // CRITICAL FIX: Filter out NEVER types before creating the union
180            // When a union member doesn't have the required property, it becomes NEVER
181            // and should be EXCLUDED from the result, not included in the union
182            let matching_non_never: Vec<TypeId> = matching
183                .into_iter()
184                .filter(|&t| t != TypeId::NEVER)
185                .collect();
186
187            if matching_non_never.is_empty() {
188                trace!("All members were NEVER, returning NEVER");
189                return TypeId::NEVER;
190            } else if matching_non_never.len() == 1 {
191                trace!(
192                    "Found single member after filtering, returning {}",
193                    matching_non_never[0].0
194                );
195                return matching_non_never[0];
196            }
197            trace!("Created union with {} members", matching_non_never.len());
198            return self.db.union(matching_non_never);
199        }
200
201        // For non-union types, check if the property exists
202        // CRITICAL: Resolve Lazy types before checking
203        let resolved_type = self.resolve_type(source_type);
204        let has_property = self.type_has_property(resolved_type, property_name);
205
206        if present {
207            // Positive: "prop" in x
208            if has_property {
209                // Property exists: Promote to required
210                let prop_type = self.get_property_type(resolved_type, property_name);
211                let required_prop = PropertyInfo {
212                    name: property_name,
213                    type_id: prop_type.unwrap_or(TypeId::UNKNOWN),
214                    write_type: prop_type.unwrap_or(TypeId::UNKNOWN),
215                    optional: false,
216                    readonly: false,
217                    is_method: false,
218                    visibility: Visibility::Public,
219                    parent_id: None,
220                };
221                let filter_obj = self.db.object(vec![required_prop]);
222                self.db.intersection2(source_type, filter_obj)
223            } else {
224                // Property not found: Narrow to never
225                // Per TypeScript: "prop in x" being true means x MUST have the property
226                // If x doesn't have it (and no index signature), narrow to never
227                TypeId::NEVER
228            }
229        } else {
230            // Negative: !("prop" in x)
231            // Exclude ONLY if property is required (not optional)
232            if self.is_property_required(resolved_type, property_name) {
233                return TypeId::NEVER;
234            }
235            // Keep source_type (no required property found, or property is optional)
236            source_type
237        }
238    }
239
240    /// Check if a type has a specific property.
241    ///
242    /// Returns true if the type has the property (required or optional),
243    /// or has an index signature that would match the property.
244    pub(crate) fn type_has_property(&self, type_id: TypeId, property_name: Atom) -> bool {
245        self.get_property_type(type_id, property_name).is_some()
246    }
247
248    /// Check if a property exists and is required on a type.
249    ///
250    /// Returns true if the property is required (not optional).
251    /// This is used for negative narrowing: `!("prop" in x)` should
252    /// exclude types where `prop` is required.
253    pub(crate) fn is_property_required(&self, type_id: TypeId, property_name: Atom) -> bool {
254        let resolved_type = self.resolve_type(type_id);
255
256        // Helper to check a specific shape
257        let check_shape = |shape_id: ObjectShapeId| -> bool {
258            let shape = self.db.object_shape(shape_id);
259            if let Some(prop) = PropertyInfo::find_in_slice(&shape.properties, property_name) {
260                return !prop.optional;
261            }
262            false
263        };
264
265        // Check standard object shape
266        if let Some(shape_id) = object_shape_id(self.db, resolved_type)
267            && check_shape(shape_id)
268        {
269            return true;
270        }
271
272        // Check object with index shape (CRITICAL for interfaces/classes)
273        if let Some(shape_id) = object_with_index_shape_id(self.db, resolved_type)
274            && check_shape(shape_id)
275        {
276            return true;
277        }
278
279        // Check intersection members
280        // If ANY member requires it, the intersection requires it
281        if let Some(members_id) = intersection_list_id(self.db, resolved_type) {
282            let members = self.db.type_list(members_id);
283            return members
284                .iter()
285                .any(|&m| self.is_property_required(m, property_name));
286        }
287
288        false
289    }
290
291    /// Get the type of a property if it exists.
292    ///
293    /// Returns Some(type) if the property exists, None otherwise.
294    pub(crate) fn get_property_type(&self, type_id: TypeId, property_name: Atom) -> Option<TypeId> {
295        // CRITICAL: Resolve Lazy types before checking for properties
296        // This ensures type aliases are resolved to their actual types
297        let resolved_type = self.resolve_type(type_id);
298
299        // Check intersection types - property exists if ANY member has it
300        if let Some(members_id) = intersection_list_id(self.db, resolved_type) {
301            let members = self.db.type_list(members_id);
302            // Return the type from the first member that has the property
303            for &member in members.iter() {
304                // Resolve each member in the intersection
305                let resolved_member = self.resolve_type(member);
306                if let Some(prop_type) = self.get_property_type(resolved_member, property_name) {
307                    return Some(prop_type);
308                }
309            }
310            return None;
311        }
312
313        // Check object shape
314        if let Some(shape_id) = object_shape_id(self.db, resolved_type) {
315            let shape = self.db.object_shape(shape_id);
316
317            // Check if the property exists in the object's properties
318            if let Some(prop) = PropertyInfo::find_in_slice(&shape.properties, property_name) {
319                return Some(prop.type_id);
320            }
321
322            // Check index signatures
323            // If the object has a string index signature, it has any string property
324            if let Some(ref string_idx) = shape.string_index {
325                // String index signature matches any string property
326                return Some(string_idx.value_type);
327            }
328
329            // If the object has a number index signature and the property name is numeric
330            if let Some(ref number_idx) = shape.number_index {
331                let prop_str = self.db.resolve_atom_ref(property_name);
332                if prop_str.chars().all(|c| c.is_ascii_digit()) {
333                    return Some(number_idx.value_type);
334                }
335            }
336
337            return None;
338        }
339
340        // Check object with index signature
341        if let Some(shape_id) = object_with_index_shape_id(self.db, resolved_type) {
342            let shape = self.db.object_shape(shape_id);
343
344            // Check properties first
345            if let Some(prop) = PropertyInfo::find_in_slice(&shape.properties, property_name) {
346                return Some(prop.type_id);
347            }
348
349            // Check index signatures
350            if let Some(ref string_idx) = shape.string_index {
351                return Some(string_idx.value_type);
352            }
353
354            if let Some(ref number_idx) = shape.number_index {
355                let prop_str = self.db.resolve_atom_ref(property_name);
356                if prop_str.chars().all(|c| c.is_ascii_digit()) {
357                    return Some(number_idx.value_type);
358                }
359            }
360
361            return None;
362        }
363
364        // For other types (functions, classes, arrays, etc.), assume they don't have arbitrary properties
365        // unless they have been handled above (object shapes, etc.)
366        None
367    }
368}