tsz_solver/narrowing_compound.rs
1//! Typeof negation, truthiness, falsy, and array narrowing.
2//!
3//! This module contains narrowing methods for:
4//! - typeof negation (excluding types by typeof result)
5//! - objectish narrowing (filtering to object-like types)
6//! - truthiness narrowing (removing falsy types)
7//! - falsy narrowing (keeping only falsy types)
8//! - `Array.isArray()` narrowing
9
10use crate::narrowing::NarrowingContext;
11use crate::narrowing_utils::NarrowingVisitor;
12use crate::subtype::{SubtypeChecker, is_subtype_of};
13use crate::type_queries::{UnionMembersKind, classify_for_union_members};
14use crate::types::{LiteralValue, TypeData, TypeId};
15use crate::visitor::{
16 TypeVisitor, intersection_list_id, literal_value, type_param_info, union_list_id,
17};
18use tracing::{Level, span};
19
20impl<'a> NarrowingContext<'a> {
21 /// Narrow a type by removing typeof-matching types.
22 ///
23 /// This is the negation of `narrow_by_typeof`.
24 /// For example, narrowing `string | number` with `typeof "string"` (sense=false)
25 /// yields `number`.
26 pub(crate) fn narrow_by_typeof_negation(
27 &self,
28 source_type: TypeId,
29 typeof_result: &str,
30 ) -> TypeId {
31 // For each typeof result, we exclude matching types
32 let excluded = match typeof_result {
33 "string" => TypeId::STRING,
34 "number" => TypeId::NUMBER,
35 "boolean" => TypeId::BOOLEAN,
36 "bigint" => TypeId::BIGINT,
37 "symbol" => TypeId::SYMBOL,
38 "undefined" => TypeId::UNDEFINED,
39 "function" => {
40 // Functions are more complex - handle separately
41 return self.narrow_excluding_function(source_type);
42 }
43 "object" => {
44 // typeof x !== "object": keep only types where typeof !== "object"
45 // Keep: primitives (string, number, boolean, bigint, symbol), undefined, void, functions
46 // Exclude: null (typeof null === "object") and object types
47 let without_null = self.narrow_excluding_type(source_type, TypeId::NULL);
48 return self.narrow_excluding_typeof_object(without_null);
49 }
50 _ => return source_type,
51 };
52
53 self.narrow_excluding_type(source_type, excluded)
54 }
55
56 /// Exclude types where `typeof` would return `"object"` from a union.
57 ///
58 /// This is used for the negation of `typeof x === "object"`.
59 /// Keeps primitives, undefined, void, and function types.
60 /// Excludes object types (objects, arrays, tuples, class instances).
61 /// Note: null should already be excluded before calling this.
62 fn narrow_excluding_typeof_object(&self, source_type: TypeId) -> TypeId {
63 let resolved = self.resolve_type(source_type);
64
65 // For non-union types, check if it's an object type
66 let Some(members) = union_list_id(self.db, resolved) else {
67 // Single type: check if typeof would be "object"
68 if self.is_typeof_object(resolved) {
69 return TypeId::NEVER;
70 }
71 return source_type;
72 };
73
74 // Filter union members: keep only non-object types
75 let members = self.db.type_list(members);
76 let kept: Vec<TypeId> = members
77 .iter()
78 .filter(|&&member| {
79 let resolved_member = self.resolve_type(member);
80 !self.is_typeof_object(resolved_member)
81 })
82 .copied()
83 .collect();
84
85 if kept.is_empty() {
86 TypeId::NEVER
87 } else if kept.len() == members.len() {
88 source_type
89 } else {
90 self.db.union(kept)
91 }
92 }
93
94 /// Check if a type would produce `"object"` from the `typeof` operator.
95 fn is_typeof_object(&self, type_id: TypeId) -> bool {
96 // Primitives and their literal types are NOT "object"
97 if matches!(
98 type_id,
99 TypeId::STRING
100 | TypeId::NUMBER
101 | TypeId::BOOLEAN
102 | TypeId::BIGINT
103 | TypeId::SYMBOL
104 | TypeId::UNDEFINED
105 | TypeId::VOID
106 | TypeId::NEVER
107 | TypeId::ANY
108 | TypeId::UNKNOWN
109 ) {
110 return false;
111 }
112
113 // Check type data for structural types
114 if let Some(data) = self.db.lookup(type_id) {
115 // Object, intersection, mapped, tuple, array: typeof === "object"
116 matches!(
117 data,
118 TypeData::Object(_)
119 | TypeData::ObjectWithIndex(_)
120 | TypeData::Intersection(_)
121 | TypeData::Mapped(_)
122 | TypeData::Tuple(_)
123 | TypeData::Array(_)
124 )
125 } else {
126 // OBJECT intrinsic: typeof === "object"
127 type_id == TypeId::OBJECT
128 }
129 }
130
131 /// Check if a type is definitely a primitive (can never pass instanceof).
132 ///
133 /// Returns true for primitive types and their literals:
134 /// string, number, boolean, bigint, symbol, undefined, void, null, never
135 fn is_definitely_primitive(&self, type_id: TypeId) -> bool {
136 // Fast path: check intrinsic primitive types
137 if matches!(
138 type_id,
139 TypeId::STRING
140 | TypeId::NUMBER
141 | TypeId::BOOLEAN
142 | TypeId::BIGINT
143 | TypeId::SYMBOL
144 | TypeId::UNDEFINED
145 | TypeId::VOID
146 | TypeId::NULL
147 | TypeId::NEVER
148 | TypeId::BOOLEAN_TRUE
149 | TypeId::BOOLEAN_FALSE
150 ) {
151 return true;
152 }
153
154 // Check for literal types (which are primitives)
155 if let Some(data) = self.db.lookup(type_id) {
156 matches!(data, TypeData::Literal(_))
157 } else {
158 false
159 }
160 }
161
162 /// Narrow a type to keep only object-like types (excluding primitives).
163 ///
164 /// This is used for instanceof fallback: if we're on the true branch of
165 /// an instanceof check but couldn't narrow to the specific instance type,
166 /// at least narrow to exclude primitives (which can never pass instanceof).
167 pub(crate) fn narrow_to_objectish(&self, source_type: TypeId) -> TypeId {
168 // ANY and UNKNOWN are kept as-is
169 if source_type == TypeId::ANY {
170 return TypeId::ANY;
171 }
172 if source_type == TypeId::UNKNOWN {
173 return TypeId::OBJECT;
174 }
175
176 let resolved = self.resolve_type(source_type);
177
178 // Handle unions: filter out primitive members
179 if let Some(members_id) = union_list_id(self.db, resolved) {
180 let members = self.db.type_list(members_id);
181 let kept: Vec<TypeId> = members
182 .iter()
183 .filter(|&&member| !self.is_definitely_primitive(member))
184 .copied()
185 .collect();
186
187 return match kept.len() {
188 0 => TypeId::NEVER,
189 1 => kept[0],
190 n if n == members.len() => source_type, // All members kept
191 _ => self.db.union(kept),
192 };
193 }
194
195 // Non-union: check if primitive
196 if self.is_definitely_primitive(resolved) {
197 TypeId::NEVER
198 } else {
199 source_type
200 }
201 }
202
203 /// Check if a type is definitely falsy.
204 ///
205 /// Returns true for: null, undefined, void, false, 0, -0, `NaN`, "", 0n
206 fn is_definitely_falsy(&self, type_id: TypeId) -> bool {
207 let resolved = self.resolve_type(type_id);
208
209 // 1. Check intrinsics that are always falsy
210 if matches!(resolved, TypeId::NULL | TypeId::UNDEFINED | TypeId::VOID) {
211 return true;
212 }
213
214 // 2. Check literals
215 if let Some(lit) = literal_value(self.db, resolved) {
216 return match lit {
217 LiteralValue::Boolean(false) => true,
218 LiteralValue::Number(n) => n.0 == 0.0 || n.0.is_nan(), // Handles 0, -0, and NaN
219 LiteralValue::String(atom) => self.db.resolve_atom_ref(atom).is_empty(), // Handles ""
220 LiteralValue::BigInt(atom) => self.db.resolve_atom_ref(atom).as_ref() == "0", // Handles 0n
221 _ => false,
222 };
223 }
224
225 false
226 }
227
228 /// Narrow an array's element type when using array.every(predicate).
229 ///
230 /// For `arr.every(isString)` where `arr: (number | string)[]` and `isString: x is string`,
231 /// this narrows the array to `string[]`.
232 ///
233 /// Only applies to array types. Non-array types are returned unchanged.
234 pub(crate) fn narrow_array_element_type(
235 &self,
236 source_type: TypeId,
237 narrowed_element: TypeId,
238 ) -> TypeId {
239 use tracing::trace;
240
241 trace!(
242 ?source_type,
243 ?narrowed_element,
244 "narrow_array_element_type called"
245 );
246
247 let resolved = self.resolve_type(source_type);
248 trace!(?resolved, "Resolved source type");
249
250 // Check if this is an array type
251 if let Some(TypeData::Array(current_elem)) = self.db.lookup(resolved) {
252 trace!(?current_elem, "Found array type");
253 // Narrow the element type
254 let new_elem = self.narrow_to_type(current_elem, narrowed_element);
255 trace!(?new_elem, "Narrowed element type");
256
257 // Reconstruct the array with narrowed element type
258 let result = self.db.array(new_elem);
259 trace!(?result, "Created narrowed array type");
260 return result;
261 }
262
263 // Check if this is a union - narrow each member that's an array
264 if let Some(TypeData::Union(list_id)) = self.db.lookup(resolved) {
265 trace!(?list_id, "Found union type");
266 let members = self.db.type_list(list_id);
267 trace!(?members, "Union members");
268 let narrowed_members: Vec<TypeId> = members
269 .iter()
270 .map(|&member| self.narrow_array_element_type(member, narrowed_element))
271 .collect();
272
273 // If any members changed, create a new union
274 if narrowed_members
275 .iter()
276 .zip(members.iter())
277 .any(|(a, b)| a != b)
278 {
279 trace!("Union members changed, creating new union");
280 return self.db.union(narrowed_members);
281 }
282 }
283
284 trace!("Not an array or union of arrays, returning unchanged");
285 // Not an array or union of arrays - return unchanged
286 source_type
287 }
288
289 /// Narrow a type by removing definitely falsy values (truthiness check).
290 ///
291 /// Narrow a type to its falsy component(s).
292 ///
293 /// This is used for the false branch of truthiness checks (e.g., `if (!x)`).
294 /// Returns the union of all falsy values that the type could be.
295 ///
296 /// Falsy values in TypeScript:
297 /// - null, undefined, void
298 /// - false (boolean literal)
299 /// - 0, -0, `NaN` (number literals)
300 /// - "" (empty string)
301 /// - 0n (bigint literal)
302 ///
303 /// CRITICAL: TypeScript does NOT narrow primitive types in falsy branches.
304 /// For `boolean`, `number`, `string`, and `bigint`, they stay as their primitive type.
305 /// For `unknown`, TypeScript does NOT narrow in falsy branches.
306 ///
307 /// Only literal types are narrowed (e.g., `0 | 1` -> `0`, `true | false` -> `false`).
308 /// Narrows a type by nullishness (like `if (x != null)` or `if (x == null)`).
309 /// If `nullish` is true, returns the nullish part (null | undefined).
310 /// If `nullish` is false, returns the non-nullish part.
311 pub fn narrow_by_nullishness(&self, source_type: TypeId, nullish: bool) -> TypeId {
312 if source_type == TypeId::ANY {
313 return source_type;
314 }
315
316 if source_type == TypeId::UNKNOWN {
317 if nullish {
318 return self.db.union(vec![TypeId::NULL, TypeId::UNDEFINED]);
319 } else {
320 let narrowed = self.narrow_excluding_type(source_type, TypeId::NULL);
321 return self.narrow_excluding_type(narrowed, TypeId::UNDEFINED);
322 }
323 }
324
325 let (non_nullish, null_part) =
326 crate::narrowing_utils::split_nullish_type(self.db, source_type);
327 if nullish {
328 null_part.unwrap_or(TypeId::NEVER)
329 } else {
330 non_nullish.unwrap_or(TypeId::NEVER)
331 }
332 }
333
334 pub fn narrow_to_falsy(&self, type_id: TypeId) -> TypeId {
335 let _span = span!(Level::TRACE, "narrow_to_falsy", type_id = type_id.0).entered();
336
337 // Handle ANY - suppresses all narrowing
338 if type_id == TypeId::ANY {
339 return TypeId::ANY;
340 }
341
342 // Handle UNKNOWN - TypeScript does NOT narrow unknown in falsy branches
343 if type_id == TypeId::UNKNOWN {
344 return TypeId::UNKNOWN;
345 }
346
347 let resolved = self.resolve_type(type_id);
348
349 // Handle Unions - recursively narrow each member and collect falsy components
350 if let UnionMembersKind::Union(members) = classify_for_union_members(self.db, resolved) {
351 let falsy_members: Vec<TypeId> = members
352 .iter()
353 .map(|&m| self.narrow_to_falsy(m))
354 .filter(|&m| m != TypeId::NEVER)
355 .collect();
356
357 return if falsy_members.is_empty() {
358 TypeId::NEVER
359 } else if falsy_members.len() == 1 {
360 falsy_members[0]
361 } else {
362 self.db.union(falsy_members)
363 };
364 }
365
366 // Handle primitive types
367 // CRITICAL: TypeScript has different behavior for different primitives
368
369 // boolean is special: it's effectively true | false, so it narrows to false
370 if resolved == TypeId::BOOLEAN {
371 return TypeId::BOOLEAN_FALSE;
372 }
373
374 // TypeScript does NOT narrow these primitives in falsy branches
375 if matches!(resolved, TypeId::STRING | TypeId::NUMBER | TypeId::BIGINT) {
376 return resolved;
377 }
378
379 // null, undefined, void are always falsy
380 if matches!(resolved, TypeId::NULL | TypeId::UNDEFINED | TypeId::VOID) {
381 return resolved;
382 }
383
384 // Handle literals - check if they're falsy
385 // This correctly handles `0` vs `1`, `""` vs `"a"`, `NaN` vs other numbers,
386 // `true` vs `false`, etc.
387 if let Some(_lit) = literal_value(self.db, resolved)
388 && self.is_definitely_falsy(resolved)
389 {
390 return type_id;
391 }
392
393 TypeId::NEVER
394 }
395
396 /// This matches TypeScript's behavior where `if (x)` narrows out:
397 /// - null, undefined, void
398 /// - false (boolean literal)
399 /// - 0, -0, `NaN` (number literals)
400 /// - "" (empty string)
401 /// - 0n (bigint literal)
402 pub fn narrow_by_truthiness(&self, source_type: TypeId) -> TypeId {
403 let _span = span!(
404 Level::TRACE,
405 "narrow_by_truthiness",
406 source_type = source_type.0
407 )
408 .entered();
409
410 // Handle special cases
411 if source_type == TypeId::ANY {
412 return source_type;
413 }
414
415 // CRITICAL FIX: unknown in truthy branch narrows to exclude null/undefined
416 // TypeScript: if (x: unknown) { x } -> x is not null | undefined
417 if source_type == TypeId::UNKNOWN {
418 let narrowed = self.narrow_excluding_type(source_type, TypeId::NULL);
419 return self.narrow_excluding_type(narrowed, TypeId::UNDEFINED);
420 }
421
422 let resolved = self.resolve_type(source_type);
423
424 // Handle Intersections (recursive)
425 // CRITICAL: If ANY part of intersection is falsy, the WHOLE intersection is falsy
426 if let Some(members_id) = intersection_list_id(self.db, resolved) {
427 let members = self.db.type_list(members_id);
428 let mut narrowed_members = Vec::with_capacity(members.len());
429
430 for &m in members.iter() {
431 let narrowed = self.narrow_by_truthiness(m);
432 // If any part is NEVER, the whole intersection is impossible
433 if narrowed == TypeId::NEVER {
434 return TypeId::NEVER;
435 }
436 narrowed_members.push(narrowed);
437 }
438
439 if narrowed_members.len() == 1 {
440 return narrowed_members[0];
441 }
442 return self.db.intersection(narrowed_members);
443 }
444
445 // Handle Unions (filter out falsy members)
446 if let Some(members_id) = union_list_id(self.db, resolved) {
447 let members = self.db.type_list(members_id);
448 let remaining: Vec<TypeId> = members
449 .iter()
450 .filter_map(|&m| {
451 let narrowed = self.narrow_by_truthiness(m);
452 if narrowed == TypeId::NEVER {
453 None
454 } else {
455 Some(narrowed)
456 }
457 })
458 .collect();
459
460 if remaining.is_empty() {
461 return TypeId::NEVER;
462 } else if remaining.len() == 1 {
463 return remaining[0];
464 }
465 return self.db.union(remaining);
466 }
467
468 // Base Case: Check if definitely falsy
469 if self.is_definitely_falsy(source_type) {
470 return TypeId::NEVER;
471 }
472
473 // Handle boolean -> true (TypeScript narrows boolean in truthy checks)
474 if resolved == TypeId::BOOLEAN {
475 return TypeId::BOOLEAN_TRUE;
476 }
477
478 // Handle Type Parameters (check constraint)
479 if let Some(info) = type_param_info(self.db, resolved)
480 && let Some(constraint) = info.constraint
481 {
482 let narrowed_constraint = self.narrow_by_truthiness(constraint);
483 if narrowed_constraint == TypeId::NEVER {
484 return TypeId::NEVER;
485 }
486 // If constraint narrowed, intersect source with it
487 if narrowed_constraint != constraint {
488 return self.db.intersection2(source_type, narrowed_constraint);
489 }
490 }
491
492 source_type
493 }
494
495 /// Narrows a type by another type using the Visitor pattern.
496 ///
497 /// This is the general-purpose narrowing function that implements the
498 /// Solver-First architecture (North Star Section 3.1). The Checker
499 /// identifies WHERE narrowing happens (AST nodes) and the Solver
500 /// calculates the RESULT.
501 ///
502 /// # Arguments
503 /// * `type_id` - The type to narrow (e.g., a union type)
504 /// * `narrower` - The type to narrow by (e.g., a literal type)
505 ///
506 /// # Returns
507 /// The narrowed type. For unions, filters to members assignable to narrower.
508 /// For type parameters, intersects with narrower.
509 ///
510 /// # Examples
511 /// - `narrow("A" | "B", "A")` → `"A"`
512 /// - `narrow(string | number, "hello")` → `"hello"`
513 /// - `narrow(T | null, undefined)` → `null` (filters out T)
514 pub fn narrow(&self, type_id: TypeId, narrower: TypeId) -> TypeId {
515 // Fast path: already a subtype
516 if is_subtype_of(self.db, type_id, narrower) {
517 return type_id;
518 }
519
520 // Use visitor to perform narrowing
521 let mut visitor = NarrowingVisitor {
522 db: self.db,
523 narrower,
524 checker: SubtypeChecker::new(self.db.as_type_database()),
525 };
526 visitor.visit_type(self.db, type_id)
527 }
528
529 /// Task 10: Narrow a type to only array-like types.
530 ///
531 /// Used for `Array.isArray(x)` in the true branch.
532 /// Keeps only arrays, tuples, and readonly arrays - preserves element types.
533 ///
534 /// # Examples
535 /// - `narrow_to_array(string[] | number)` → `string[]`
536 /// - `narrow_to_array(unknown)` → `any[]`
537 /// - `narrow_to_array(any)` → `any`
538 /// - `narrow_to_array(readonly [number, string])` → `readonly [number, string]`
539 pub(crate) fn narrow_to_array(&self, source_type: TypeId) -> TypeId {
540 // Handle ANY and UNKNOWN first
541 if source_type == TypeId::ANY {
542 return TypeId::ANY;
543 }
544
545 if source_type == TypeId::UNKNOWN {
546 // Unknown narrows to any[] (most general array type)
547 return self.db.array(TypeId::ANY);
548 }
549
550 // Handle Union: filter members, keeping only array-like types
551 if let Some(members) = union_list_id(self.db, source_type) {
552 let members = self.db.type_list(members);
553 let array_like: Vec<TypeId> = members
554 .iter()
555 .filter_map(|&member| {
556 let narrowed = self.narrow_to_array(member);
557 if narrowed == TypeId::NEVER {
558 None
559 } else {
560 Some(narrowed)
561 }
562 })
563 .collect();
564
565 if array_like.is_empty() {
566 return TypeId::NEVER;
567 } else if array_like.len() == 1 {
568 return array_like[0];
569 }
570 return self.db.union(array_like);
571 }
572
573 // Handle Intersections: if ANY member is array-like, the whole intersection is array-like
574 // e.g., string[] & { foo: string } is an array-like type
575 if let Some(members_id) = intersection_list_id(self.db, source_type) {
576 let members = self.db.type_list(members_id);
577 let is_array = members.iter().any(|&m| {
578 let resolved = self.resolve_type(m);
579 self.is_array_like(resolved) || self.narrow_to_array(resolved) != TypeId::NEVER
580 });
581
582 if is_array {
583 return source_type;
584 }
585 }
586
587 // Handle Type Parameters: intersect with any[]
588 if let Some(_info) = type_param_info(self.db, source_type) {
589 let any_array = self.db.array(TypeId::ANY);
590 return self.db.intersection2(source_type, any_array);
591 }
592
593 // Check if type is array-like (Array, Tuple, or ReadonlyArray)
594 if self.is_array_like(source_type) {
595 return source_type;
596 }
597
598 // Not array-like
599 TypeId::NEVER
600 }
601
602 /// Task 10: Exclude array-like types from a type.
603 ///
604 /// Used for `!Array.isArray(x)` in the false branch.
605 /// Removes arrays, tuples, and readonly arrays.
606 ///
607 /// # Examples
608 /// - `narrow_excluding_array(string[] | number)` → `number`
609 /// - `narrow_excluding_array(string[])` → `NEVER`
610 /// - `narrow_excluding_array(unknown)` → `unknown`
611 pub(crate) fn narrow_excluding_array(&self, source_type: TypeId) -> TypeId {
612 // Handle ANY and UNKNOWN
613 if source_type == TypeId::ANY {
614 return TypeId::ANY;
615 }
616
617 if source_type == TypeId::UNKNOWN {
618 // Unknown doesn't have a "not array" type representation
619 return TypeId::UNKNOWN;
620 }
621
622 // Handle Union: filter out array-like members
623 if let Some(members) = union_list_id(self.db, source_type) {
624 let members = self.db.type_list(members);
625 let non_array: Vec<TypeId> = members
626 .iter()
627 .filter_map(|&member| {
628 let narrowed = self.narrow_excluding_array(member);
629 if narrowed == TypeId::NEVER {
630 None
631 } else {
632 Some(narrowed)
633 }
634 })
635 .collect();
636
637 if non_array.is_empty() {
638 return TypeId::NEVER;
639 } else if non_array.len() == 1 {
640 return non_array[0];
641 }
642 return self.db.union(non_array);
643 }
644
645 // Handle Type Parameters: check if constraint is definitely an array
646 // e.g., if T extends string[] and we check !Array.isArray(x), then x is never
647 if let Some(info) = type_param_info(self.db, source_type)
648 && let Some(constraint) = info.constraint
649 {
650 // If the constraint is definitely an array, then T is definitely an array.
651 // So !Array.isArray(T) is NEVER.
652 let narrowed_constraint = self.narrow_excluding_array(constraint);
653 if narrowed_constraint == TypeId::NEVER {
654 return TypeId::NEVER;
655 }
656 }
657
658 // If array-like, return NEVER (excluded)
659 if self.is_array_like(source_type) {
660 return TypeId::NEVER;
661 }
662
663 // Not array-like, keep as-is
664 source_type
665 }
666
667 /// Check if a type is array-like (Array, Tuple, or `ReadonlyArray`).
668 ///
669 /// This unwraps `ReadonlyType` recursively to check the underlying type.
670 pub(crate) fn is_array_like(&self, type_id: TypeId) -> bool {
671 use crate::type_queries;
672
673 // Check for ReadonlyType wrapper (unwrap recursively)
674 if let Some(TypeData::ReadonlyType(inner)) = self.db.lookup(type_id) {
675 return self.is_array_like(inner);
676 }
677
678 // Check if type is Array, Tuple, or ReadonlyArray (wrapped)
679 type_queries::is_array_type(self.db, type_id)
680 || type_queries::is_tuple_type(self.db, type_id)
681 }
682}