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 super::NarrowingContext;
11use super::utils::NarrowingVisitor;
12use crate::relations::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 resolved.is_nullable() {
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) = super::utils::split_nullish_type(self.db, source_type);
326 if nullish {
327 null_part.unwrap_or(TypeId::NEVER)
328 } else {
329 non_nullish.unwrap_or(TypeId::NEVER)
330 }
331 }
332
333 pub fn narrow_to_falsy(&self, type_id: TypeId) -> TypeId {
334 let _span = span!(Level::TRACE, "narrow_to_falsy", type_id = type_id.0).entered();
335
336 // Handle ANY - suppresses all narrowing
337 if type_id == TypeId::ANY {
338 return TypeId::ANY;
339 }
340
341 // Handle UNKNOWN - TypeScript does NOT narrow unknown in falsy branches
342 if type_id == TypeId::UNKNOWN {
343 return TypeId::UNKNOWN;
344 }
345
346 let resolved = self.resolve_type(type_id);
347
348 // Handle Unions - recursively narrow each member and collect falsy components
349 if let UnionMembersKind::Union(members) = classify_for_union_members(self.db, resolved) {
350 let falsy_members: Vec<TypeId> = members
351 .iter()
352 .map(|&m| self.narrow_to_falsy(m))
353 .filter(|&m| m != TypeId::NEVER)
354 .collect();
355
356 return if falsy_members.is_empty() {
357 TypeId::NEVER
358 } else if falsy_members.len() == 1 {
359 falsy_members[0]
360 } else {
361 self.db.union(falsy_members)
362 };
363 }
364
365 // Handle primitive types
366 // CRITICAL: TypeScript has different behavior for different primitives
367
368 // boolean is special: it's effectively true | false, so it narrows to false
369 if resolved == TypeId::BOOLEAN {
370 return TypeId::BOOLEAN_FALSE;
371 }
372
373 // TypeScript does NOT narrow these primitives in falsy branches
374 if matches!(resolved, TypeId::STRING | TypeId::NUMBER | TypeId::BIGINT) {
375 return resolved;
376 }
377
378 // null, undefined, void are always falsy
379 if resolved.is_nullable() {
380 return resolved;
381 }
382
383 // Handle literals - check if they're falsy
384 // This correctly handles `0` vs `1`, `""` vs `"a"`, `NaN` vs other numbers,
385 // `true` vs `false`, etc.
386 if let Some(_lit) = literal_value(self.db, resolved)
387 && self.is_definitely_falsy(resolved)
388 {
389 return type_id;
390 }
391
392 TypeId::NEVER
393 }
394
395 /// This matches TypeScript's behavior where `if (x)` narrows out:
396 /// - null, undefined, void
397 /// - false (boolean literal)
398 /// - 0, -0, `NaN` (number literals)
399 /// - "" (empty string)
400 /// - 0n (bigint literal)
401 pub fn narrow_by_truthiness(&self, source_type: TypeId) -> TypeId {
402 let _span = span!(
403 Level::TRACE,
404 "narrow_by_truthiness",
405 source_type = source_type.0
406 )
407 .entered();
408
409 // Handle special cases
410 if source_type == TypeId::ANY {
411 return source_type;
412 }
413
414 // CRITICAL FIX: unknown in truthy branch narrows to exclude null/undefined
415 // TypeScript: if (x: unknown) { x } -> x is not null | undefined
416 if source_type == TypeId::UNKNOWN {
417 let narrowed = self.narrow_excluding_type(source_type, TypeId::NULL);
418 return self.narrow_excluding_type(narrowed, TypeId::UNDEFINED);
419 }
420
421 let resolved = self.resolve_type(source_type);
422
423 // Handle Intersections (recursive)
424 // CRITICAL: If ANY part of intersection is falsy, the WHOLE intersection is falsy
425 if let Some(members_id) = intersection_list_id(self.db, resolved) {
426 let members = self.db.type_list(members_id);
427 let mut narrowed_members = Vec::with_capacity(members.len());
428
429 for &m in members.iter() {
430 let narrowed = self.narrow_by_truthiness(m);
431 // If any part is NEVER, the whole intersection is impossible
432 if narrowed == TypeId::NEVER {
433 return TypeId::NEVER;
434 }
435 narrowed_members.push(narrowed);
436 }
437
438 if narrowed_members.len() == 1 {
439 return narrowed_members[0];
440 }
441 return self.db.intersection(narrowed_members);
442 }
443
444 // Handle Unions (filter out falsy members)
445 if let Some(members_id) = union_list_id(self.db, resolved) {
446 let members = self.db.type_list(members_id);
447 let remaining: Vec<TypeId> = members
448 .iter()
449 .filter_map(|&m| {
450 let narrowed = self.narrow_by_truthiness(m);
451 if narrowed == TypeId::NEVER {
452 None
453 } else {
454 Some(narrowed)
455 }
456 })
457 .collect();
458
459 if remaining.is_empty() {
460 return TypeId::NEVER;
461 } else if remaining.len() == 1 {
462 return remaining[0];
463 }
464 return self.db.union(remaining);
465 }
466
467 // Base Case: Check if definitely falsy
468 if self.is_definitely_falsy(source_type) {
469 return TypeId::NEVER;
470 }
471
472 // Handle boolean -> true (TypeScript narrows boolean in truthy checks)
473 if resolved == TypeId::BOOLEAN {
474 return TypeId::BOOLEAN_TRUE;
475 }
476
477 // Handle Type Parameters (check constraint)
478 if let Some(info) = type_param_info(self.db, resolved)
479 && let Some(constraint) = info.constraint
480 {
481 let narrowed_constraint = self.narrow_by_truthiness(constraint);
482 if narrowed_constraint == TypeId::NEVER {
483 return TypeId::NEVER;
484 }
485 // If constraint narrowed, intersect source with it
486 if narrowed_constraint != constraint {
487 return self.db.intersection2(source_type, narrowed_constraint);
488 }
489 }
490
491 source_type
492 }
493
494 /// Narrows a type by another type using the Visitor pattern.
495 ///
496 /// This is the general-purpose narrowing function that implements the
497 /// Solver-First architecture (North Star Section 3.1). The Checker
498 /// identifies WHERE narrowing happens (AST nodes) and the Solver
499 /// calculates the RESULT.
500 ///
501 /// # Arguments
502 /// * `type_id` - The type to narrow (e.g., a union type)
503 /// * `narrower` - The type to narrow by (e.g., a literal type)
504 ///
505 /// # Returns
506 /// The narrowed type. For unions, filters to members assignable to narrower.
507 /// For type parameters, intersects with narrower.
508 ///
509 /// # Examples
510 /// - `narrow("A" | "B", "A")` → `"A"`
511 /// - `narrow(string | number, "hello")` → `"hello"`
512 /// - `narrow(T | null, undefined)` → `null` (filters out T)
513 pub fn narrow(&self, type_id: TypeId, narrower: TypeId) -> TypeId {
514 // Fast path: already a subtype
515 if is_subtype_of(self.db, type_id, narrower) {
516 return type_id;
517 }
518
519 // Use visitor to perform narrowing
520 let mut visitor = NarrowingVisitor {
521 db: self.db,
522 narrower,
523 checker: SubtypeChecker::new(self.db.as_type_database()),
524 };
525 visitor.visit_type(self.db, type_id)
526 }
527
528 /// Task 10: Narrow a type to only array-like types.
529 ///
530 /// Used for `Array.isArray(x)` in the true branch.
531 /// Keeps only arrays, tuples, and readonly arrays - preserves element types.
532 ///
533 /// # Examples
534 /// - `narrow_to_array(string[] | number)` → `string[]`
535 /// - `narrow_to_array(unknown)` → `any[]`
536 /// - `narrow_to_array(any)` → `any`
537 /// - `narrow_to_array(readonly [number, string])` → `readonly [number, string]`
538 pub(crate) fn narrow_to_array(&self, source_type: TypeId) -> TypeId {
539 // Handle ANY and UNKNOWN first
540 if source_type == TypeId::ANY {
541 return TypeId::ANY;
542 }
543
544 if source_type == TypeId::UNKNOWN {
545 // Unknown narrows to any[] (most general array type)
546 return self.db.array(TypeId::ANY);
547 }
548
549 // Handle Union: filter members, keeping only array-like types
550 if let Some(members) = union_list_id(self.db, source_type) {
551 let members = self.db.type_list(members);
552 let array_like: Vec<TypeId> = members
553 .iter()
554 .filter_map(|&member| {
555 let narrowed = self.narrow_to_array(member);
556 if narrowed == TypeId::NEVER {
557 None
558 } else {
559 Some(narrowed)
560 }
561 })
562 .collect();
563
564 if array_like.is_empty() {
565 return TypeId::NEVER;
566 } else if array_like.len() == 1 {
567 return array_like[0];
568 }
569 return self.db.union(array_like);
570 }
571
572 // Handle Intersections: if ANY member is array-like, the whole intersection is array-like
573 // e.g., string[] & { foo: string } is an array-like type
574 if let Some(members_id) = intersection_list_id(self.db, source_type) {
575 let members = self.db.type_list(members_id);
576 let is_array = members.iter().any(|&m| {
577 let resolved = self.resolve_type(m);
578 self.is_array_like(resolved) || self.narrow_to_array(resolved) != TypeId::NEVER
579 });
580
581 if is_array {
582 return source_type;
583 }
584 }
585
586 // Handle Type Parameters: intersect with any[]
587 if let Some(_info) = type_param_info(self.db, source_type) {
588 let any_array = self.db.array(TypeId::ANY);
589 return self.db.intersection2(source_type, any_array);
590 }
591
592 // Check if type is array-like (Array, Tuple, or ReadonlyArray)
593 if self.is_array_like(source_type) {
594 return source_type;
595 }
596
597 // Not array-like
598 TypeId::NEVER
599 }
600
601 /// Task 10: Exclude array-like types from a type.
602 ///
603 /// Used for `!Array.isArray(x)` in the false branch.
604 /// Removes arrays, tuples, and readonly arrays.
605 ///
606 /// # Examples
607 /// - `narrow_excluding_array(string[] | number)` → `number`
608 /// - `narrow_excluding_array(string[])` → `NEVER`
609 /// - `narrow_excluding_array(unknown)` → `unknown`
610 pub(crate) fn narrow_excluding_array(&self, source_type: TypeId) -> TypeId {
611 // Handle ANY and UNKNOWN
612 if source_type == TypeId::ANY {
613 return TypeId::ANY;
614 }
615
616 if source_type == TypeId::UNKNOWN {
617 // Unknown doesn't have a "not array" type representation
618 return TypeId::UNKNOWN;
619 }
620
621 // Handle Union: filter out array-like members
622 if let Some(members) = union_list_id(self.db, source_type) {
623 let members = self.db.type_list(members);
624 let non_array: Vec<TypeId> = members
625 .iter()
626 .filter_map(|&member| {
627 let narrowed = self.narrow_excluding_array(member);
628 if narrowed == TypeId::NEVER {
629 None
630 } else {
631 Some(narrowed)
632 }
633 })
634 .collect();
635
636 if non_array.is_empty() {
637 return TypeId::NEVER;
638 } else if non_array.len() == 1 {
639 return non_array[0];
640 }
641 return self.db.union(non_array);
642 }
643
644 // Handle Type Parameters: check if constraint is definitely an array
645 // e.g., if T extends string[] and we check !Array.isArray(x), then x is never
646 if let Some(info) = type_param_info(self.db, source_type)
647 && let Some(constraint) = info.constraint
648 {
649 // If the constraint is definitely an array, then T is definitely an array.
650 // So !Array.isArray(T) is NEVER.
651 let narrowed_constraint = self.narrow_excluding_array(constraint);
652 if narrowed_constraint == TypeId::NEVER {
653 return TypeId::NEVER;
654 }
655 }
656
657 // If array-like, return NEVER (excluded)
658 if self.is_array_like(source_type) {
659 return TypeId::NEVER;
660 }
661
662 // Not array-like, keep as-is
663 source_type
664 }
665
666 /// Check if a type is array-like (Array, Tuple, or `ReadonlyArray`).
667 ///
668 /// This unwraps `ReadonlyType` recursively to check the underlying type.
669 pub(crate) fn is_array_like(&self, type_id: TypeId) -> bool {
670 use crate::type_queries;
671
672 // Check for ReadonlyType wrapper (unwrap recursively)
673 if let Some(TypeData::ReadonlyType(inner)) = self.db.lookup(type_id) {
674 return self.is_array_like(inner);
675 }
676
677 // Check if type is Array, Tuple, or ReadonlyArray (wrapped)
678 type_queries::is_array_type(self.db, type_id)
679 || type_queries::is_tuple_type(self.db, type_id)
680 }
681}