Skip to main content

tsz_solver/narrowing/
instanceof.rs

1//! instanceof-based type narrowing methods.
2//!
3//! Extracted from `mod.rs` to keep individual files under the 2000 LOC threshold.
4//! Contains the three core instanceof narrowing entry points:
5//! - `narrow_by_instanceof` — dispatches on constructor type shape to extract
6//!   the instance type, then filters unions / falls back to exclusion.
7//! - `narrow_by_instance_type` — filters unions using instanceof-specific
8//!   semantics (type-parameter intersection, primitive exclusion).
9//! - `narrow_by_instanceof_false` — false-branch narrowing: keeps primitives,
10//!   excludes subtypes of the instance type.
11
12use super::NarrowingContext;
13use crate::relations::subtype::SubtypeChecker;
14use crate::type_queries::{InstanceTypeKind, classify_for_instance_type};
15use crate::types::TypeId;
16use crate::utils::{TypeIdExt, intersection_or_single, union_or_single};
17use crate::visitor::union_list_id;
18use tracing::{Level, span, trace};
19
20impl<'a> NarrowingContext<'a> {
21    /// Narrow a type based on an instanceof check.
22    ///
23    /// Example: `x instanceof MyClass` narrows `A | B` to include only `A` where `A` is an instance of `MyClass`
24    pub fn narrow_by_instanceof(
25        &self,
26        source_type: TypeId,
27        constructor_type: TypeId,
28        sense: bool,
29    ) -> TypeId {
30        let _span = span!(
31            Level::TRACE,
32            "narrow_by_instanceof",
33            source_type = source_type.0,
34            constructor_type = constructor_type.0,
35            sense
36        )
37        .entered();
38
39        // TODO: Check for static [Symbol.hasInstance] method which overrides standard narrowing
40        // TypeScript allows classes to define custom instanceof behavior via:
41        //   static [Symbol.hasInstance](value: any): boolean
42        // This would require evaluating method calls and type predicates, which is
43        // significantly more complex than the standard construct signature approach.
44
45        // CRITICAL: Resolve Lazy types for both source and constructor
46        // This ensures type aliases are resolved to their actual types
47        let resolved_source = self.resolve_type(source_type);
48        let resolved_constructor = self.resolve_type(constructor_type);
49
50        // Extract the instance type from the constructor
51        let instance_type = match classify_for_instance_type(self.db, resolved_constructor) {
52            InstanceTypeKind::Callable(shape_id) => {
53                // For callable types with construct signatures, get the return type of the construct signature
54                let shape = self.db.callable_shape(shape_id);
55                // Find a construct signature and get its return type (the instance type)
56                if let Some(construct_sig) = shape.construct_signatures.first() {
57                    construct_sig.return_type
58                } else {
59                    // No construct signature found, can't narrow
60                    trace!("No construct signature found in callable type");
61                    return source_type;
62                }
63            }
64            InstanceTypeKind::Function(shape_id) => {
65                // For function types, check if it's a constructor
66                let shape = self.db.function_shape(shape_id);
67                if shape.is_constructor {
68                    // The return type is the instance type
69                    shape.return_type
70                } else {
71                    trace!("Function is not a constructor");
72                    return source_type;
73                }
74            }
75            InstanceTypeKind::Intersection(members) => {
76                // For intersection types, we need to extract instance types from all members
77                // For now, create an intersection of the instance types
78                let instance_types: Vec<TypeId> = members
79                    .iter()
80                    .map(|&member| self.narrow_by_instanceof(source_type, member, sense))
81                    .collect();
82
83                if sense {
84                    intersection_or_single(self.db, instance_types)
85                } else {
86                    // For negation with intersection, we can't easily exclude
87                    // Fall back to returning the source type unchanged
88                    source_type
89                }
90            }
91            InstanceTypeKind::Union(members) => {
92                // For union types, extract instance types from all members
93                let instance_types: Vec<TypeId> = members
94                    .iter()
95                    .filter_map(|&member| {
96                        self.narrow_by_instanceof(source_type, member, sense)
97                            .non_never()
98                    })
99                    .collect();
100
101                if sense {
102                    union_or_single(self.db, instance_types)
103                } else {
104                    // For negation with union, we can't easily exclude
105                    // Fall back to returning the source type unchanged
106                    source_type
107                }
108            }
109            InstanceTypeKind::Readonly(inner) => {
110                // Readonly wrapper - extract from inner type
111                return self.narrow_by_instanceof(source_type, inner, sense);
112            }
113            InstanceTypeKind::TypeParameter { constraint } => {
114                // Follow type parameter constraint
115                if let Some(constraint) = constraint {
116                    return self.narrow_by_instanceof(source_type, constraint, sense);
117                }
118                trace!("Type parameter has no constraint");
119                return source_type;
120            }
121            InstanceTypeKind::SymbolRef(_) | InstanceTypeKind::NeedsEvaluation => {
122                // Complex cases that need further evaluation
123                // For now, return the source type unchanged
124                trace!("Complex instance type (SymbolRef or NeedsEvaluation), returning unchanged");
125                return source_type;
126            }
127            InstanceTypeKind::NotConstructor => {
128                trace!("Constructor type is not a valid constructor");
129                return source_type;
130            }
131        };
132
133        // Now narrow based on the sense (positive or negative)
134        if sense {
135            // CRITICAL: instanceof DOES narrow any/unknown (unlike equality checks)
136            if resolved_source == TypeId::ANY {
137                // any narrows to the instance type with instanceof
138                trace!("Narrowing any to instance type via instanceof");
139                return instance_type;
140            }
141
142            if resolved_source == TypeId::UNKNOWN {
143                // unknown narrows to the instance type with instanceof
144                trace!("Narrowing unknown to instance type via instanceof");
145                return instance_type;
146            }
147
148            // Handle Union: filter members based on instanceof relationship
149            if let Some(members_id) = union_list_id(self.db, resolved_source) {
150                let members = self.db.type_list(members_id);
151                // PERF: Reuse a single SubtypeChecker across all member checks
152                // instead of allocating 4 hash sets per is_subtype_of call.
153                let mut checker = SubtypeChecker::new(self.db.as_type_database());
154                let mut filtered_members: Vec<TypeId> = Vec::new();
155                for &member in &*members {
156                    // Check if member is assignable to instance type
157                    checker.reset();
158                    if checker.is_subtype_of(member, instance_type) {
159                        trace!(
160                            "Union member {} is assignable to instance type {}, keeping",
161                            member.0, instance_type.0
162                        );
163                        filtered_members.push(member);
164                        continue;
165                    }
166
167                    // Check if instance type is assignable to member (subclass case)
168                    // If we have a Dog and instanceof Animal, Dog is an instance of Animal
169                    checker.reset();
170                    if checker.is_subtype_of(instance_type, member) {
171                        trace!(
172                            "Instance type {} is assignable to union member {} (subclass), narrowing to instance type",
173                            instance_type.0, member.0
174                        );
175                        filtered_members.push(instance_type);
176                        continue;
177                    }
178
179                    // Interface overlap: both are object-like but not assignable
180                    // Use intersection to preserve properties from both
181                    if self.are_object_like(member) && self.are_object_like(instance_type) {
182                        trace!(
183                            "Interface overlap between {} and {}, using intersection",
184                            member.0, instance_type.0
185                        );
186                        filtered_members.push(self.db.intersection2(member, instance_type));
187                        continue;
188                    }
189
190                    trace!("Union member {} excluded by instanceof check", member.0);
191                }
192
193                union_or_single(self.db, filtered_members)
194            } else {
195                // Non-union type: use standard narrowing with intersection fallback
196                let narrowed = self.narrow_to_type(resolved_source, instance_type);
197
198                // If that returns NEVER, try intersection approach for interface vs class cases
199                // In TypeScript, instanceof on an interface narrows to intersection, not NEVER
200                if narrowed == TypeId::NEVER && resolved_source != TypeId::NEVER {
201                    // Check for interface overlap before using intersection
202                    if self.are_object_like(resolved_source) && self.are_object_like(instance_type)
203                    {
204                        trace!("Interface vs class detected, using intersection instead of NEVER");
205                        self.db.intersection2(resolved_source, instance_type)
206                    } else {
207                        narrowed
208                    }
209                } else {
210                    narrowed
211                }
212            }
213        } else {
214            // Negative: !(x instanceof Constructor) - exclude the instance type
215            // For unions, exclude members that are subtypes of the instance type
216            if let Some(members_id) = union_list_id(self.db, resolved_source) {
217                let members = self.db.type_list(members_id);
218                // PERF: Reuse a single SubtypeChecker across all member checks
219                let mut checker = SubtypeChecker::new(self.db.as_type_database());
220                let mut filtered_members: Vec<TypeId> = Vec::new();
221                for &member in &*members {
222                    // Exclude members that are definitely subtypes of the instance type
223                    checker.reset();
224                    if !checker.is_subtype_of(member, instance_type) {
225                        filtered_members.push(member);
226                    }
227                }
228
229                union_or_single(self.db, filtered_members)
230            } else {
231                // Non-union: use standard exclusion
232                self.narrow_excluding_type(resolved_source, instance_type)
233            }
234        }
235    }
236
237    /// Narrow a type by instanceof check using the instance type.
238    ///
239    /// Unlike `narrow_to_type` which uses structural assignability to filter union members,
240    /// this method uses instanceof-specific semantics:
241    /// - Type parameters with constraints assignable to the target are kept (intersected)
242    /// - When a type parameter absorbs the target, anonymous object types are excluded
243    ///   since they cannot be class instances at runtime
244    ///
245    /// This prevents anonymous object types like `{ x: string }` from surviving instanceof
246    /// narrowing when they happen to be structurally compatible with the class type.
247    pub fn narrow_by_instance_type(&self, source_type: TypeId, instance_type: TypeId) -> TypeId {
248        let resolved_source = self.resolve_type(source_type);
249
250        if resolved_source == TypeId::ERROR && source_type != TypeId::ERROR {
251            return source_type;
252        }
253
254        let resolved_target = self.resolve_type(instance_type);
255        if resolved_target == TypeId::ERROR && instance_type != TypeId::ERROR {
256            return source_type;
257        }
258
259        if resolved_source == resolved_target {
260            return source_type;
261        }
262
263        // any/unknown narrow to instance type with instanceof
264        if resolved_source == TypeId::ANY || resolved_source == TypeId::UNKNOWN {
265            return instance_type;
266        }
267
268        // If source is a union, filter members using instanceof semantics
269        if let Some(members) = union_list_id(self.db, resolved_source) {
270            let members = self.db.type_list(members);
271            trace!(
272                "instanceof: narrowing union with {} members {:?} to instance type {}",
273                members.len(),
274                members.iter().map(|m| m.0).collect::<Vec<_>>(),
275                instance_type.0
276            );
277
278            // First pass: check if any type parameter matches the instance type.
279            let mut type_param_results: Vec<(usize, TypeId)> = Vec::new();
280            for (i, &member) in members.iter().enumerate() {
281                if let Some(narrowed) = self.narrow_type_param(member, instance_type) {
282                    type_param_results.push((i, narrowed));
283                }
284            }
285
286            let matching: Vec<TypeId> = if !type_param_results.is_empty() {
287                // Type parameter(s) matched: keep type params and exclude anonymous
288                // object types that can't be class instances at runtime.
289                let mut result = Vec::new();
290                let tp_indices: Vec<usize> = type_param_results.iter().map(|(i, _)| *i).collect();
291                for &(_, narrowed) in &type_param_results {
292                    result.push(narrowed);
293                }
294                for (i, &member) in members.iter().enumerate() {
295                    if tp_indices.contains(&i) {
296                        continue;
297                    }
298                    if crate::type_queries::is_object_type(self.db, member) {
299                        trace!(
300                            "instanceof: excluding anonymous object {} (type param absorbs)",
301                            member.0
302                        );
303                        continue;
304                    }
305                    if crate::relations::subtype::is_subtype_of_with_db(
306                        self.db,
307                        member,
308                        instance_type,
309                    ) {
310                        result.push(member);
311                    } else if crate::relations::subtype::is_subtype_of_with_db(
312                        self.db,
313                        instance_type,
314                        member,
315                    ) {
316                        result.push(instance_type);
317                    }
318                }
319                result
320            } else {
321                // No type parameter match: filter by instanceof semantics.
322                // Primitives can never pass instanceof; non-primitives are
323                // checked for assignability with the instance type.
324                members
325                    .iter()
326                    .filter_map(|&member| {
327                        // Primitive types can never pass `instanceof` at runtime.
328                        if self.is_js_primitive(member) {
329                            return None;
330                        }
331                        if let Some(narrowed) = self.narrow_type_param(member, instance_type) {
332                            return Some(narrowed);
333                        }
334                        // Member assignable to instance type → keep member
335                        if self.is_assignable_to(member, instance_type) {
336                            return Some(member);
337                        }
338                        // Instance type assignable to member → narrow to instance
339                        // (e.g., member=Animal, instance=Dog → Dog)
340                        if self.is_assignable_to(instance_type, member) {
341                            return Some(instance_type);
342                        }
343                        // Neither direction holds — create intersection per tsc
344                        // semantics. This handles cases like Date instanceof Object
345                        // where assignability checks may miss the relationship.
346                        // The intersection preserves the member's shape while
347                        // constraining it to the instance type.
348                        Some(self.db.intersection2(member, instance_type))
349                    })
350                    .collect()
351            };
352
353            if matching.is_empty() {
354                return self.narrow_to_type(source_type, instance_type);
355            } else if matching.len() == 1 {
356                return matching[0];
357            }
358            return self.db.union(matching);
359        }
360
361        // Non-union: use instanceof-specific semantics
362        trace!(
363            "instanceof: non-union path for source_type={}",
364            source_type.0
365        );
366
367        // Try type parameter narrowing first (produces T & InstanceType)
368        if let Some(narrowed) = self.narrow_type_param(resolved_source, instance_type) {
369            return narrowed;
370        }
371
372        // For non-primitive, non-type-param source types, instanceof narrowing
373        // should keep them when there's a potential runtime relationship.
374        // This handles cases like `readonly number[]` narrowed by `instanceof Array`:
375        // - readonly number[] is NOT a subtype of Array<T> (missing mutating methods)
376        // - Array<T> is NOT a subtype of readonly number[] (unbound T)
377        // - But at runtime, a readonly array IS an Array instance
378        if !self.is_js_primitive(resolved_source) {
379            if self.is_assignable_to(resolved_source, instance_type) {
380                return source_type;
381            }
382            if self.is_assignable_to(instance_type, resolved_source) {
383                return instance_type;
384            }
385            // Non-primitive types may still be instances at runtime.
386            // Neither direction holds — create intersection per tsc semantics.
387            // This handles cases like `interface I {}` narrowed by `instanceof RegExp`.
388            return self.db.intersection2(source_type, instance_type);
389        }
390        // Primitives can never pass instanceof
391        TypeId::NEVER
392    }
393
394    /// Narrow a type for the false branch of `instanceof`.
395    ///
396    /// Keeps primitive types (which can never pass instanceof) and excludes
397    /// non-primitive members that are subtypes of the instance type.
398    /// For example, `string | number | Date` with `instanceof Object` false
399    /// branch gives `string | number` (Date is excluded as it's an Object instance).
400    pub fn narrow_by_instanceof_false(&self, source_type: TypeId, instance_type: TypeId) -> TypeId {
401        let resolved_source = self.resolve_type(source_type);
402
403        if let Some(members) = union_list_id(self.db, resolved_source) {
404            let members = self.db.type_list(members);
405            let remaining: Vec<TypeId> = members
406                .iter()
407                .filter(|&&member| {
408                    // Primitives always survive the false branch of instanceof
409                    if self.is_js_primitive(member) {
410                        return true;
411                    }
412                    // A member only fails to reach the false branch if it is GUARANTEED
413                    // to pass the true branch. In TypeScript, this means the member
414                    // is assignable to the instance type.
415                    // If it is NOT assignable, it MIGHT fail at runtime, so we MUST keep it.
416                    !self.is_assignable_to(member, instance_type)
417                })
418                .copied()
419                .collect();
420
421            if remaining.is_empty() {
422                return TypeId::NEVER;
423            } else if remaining.len() == 1 {
424                return remaining[0];
425            }
426            return self.db.union(remaining);
427        }
428
429        // Non-union: if it's guaranteed to be an instance, it will never reach the false branch.
430        if self.is_assignable_to(resolved_source, instance_type) {
431            return TypeId::NEVER;
432        }
433
434        // Otherwise, it might reach the false branch, so we keep the original type.
435        source_type
436    }
437}