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}