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