Skip to main content

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}