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 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}