Skip to main content

tsz_solver/visitors/
visitor_predicates.rs

1//! Type Predicate Functions
2//!
3//! This module provides convenience functions for checking type classifications
4//! and querying whether types contain specific nested type kinds. These are
5//! extracted from the main visitor module for maintainability.
6//!
7//! # Categories
8//!
9//! - **Simple predicates** (`is_*`): Check if a type matches a specific `TypeData` variant.
10//! - **Deep predicates** (`contains_*`): Recursively check if a type contains specific nested types.
11//! - **Database wrappers** (`*_db`): Variants that unwrap through `ReadonlyType`/`NoInfer`/constraints.
12//! - **Object classification**: `ObjectTypeKind` enum and `classify_object_type`.
13
14use crate::types::{IntrinsicKind, ObjectShapeId};
15use crate::{TypeData, TypeDatabase, TypeId};
16use rustc_hash::FxHashMap;
17
18// =============================================================================
19// Specialized Type Predicate Visitors
20// =============================================================================
21
22/// Check if a type is a literal type.
23///
24/// Matches: `TypeData::Literal`(_)
25pub fn is_literal_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
26    matches!(types.lookup(type_id), Some(TypeData::Literal(_)))
27}
28
29/// Check if a type is a module namespace type (import * as ns).
30///
31/// Matches: `TypeData::ModuleNamespace`(_)
32pub fn is_module_namespace_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
33    matches!(types.lookup(type_id), Some(TypeData::ModuleNamespace(_)))
34}
35
36/// Check if a type is a function type (Function or Callable).
37///
38/// This also handles intersections containing function types.
39pub fn is_function_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
40    is_function_type_impl(types, type_id)
41}
42
43fn is_function_type_impl(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
44    match types.lookup(type_id) {
45        Some(TypeData::Function(_) | TypeData::Callable(_)) => true,
46        Some(TypeData::Intersection(members)) => {
47            let members = types.type_list(members);
48            members
49                .iter()
50                .any(|&member| is_function_type_impl(types, member))
51        }
52        _ => false,
53    }
54}
55
56/// Check if a type is an object-like type (suitable for typeof "object").
57///
58/// Returns true for: Object, `ObjectWithIndex`, Array, Tuple, Mapped, `ReadonlyType` (of object)
59pub fn is_object_like_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
60    is_object_like_type_impl(types, type_id)
61}
62
63fn is_object_like_type_impl(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
64    match types.lookup(type_id) {
65        Some(
66            TypeData::Object(_)
67            | TypeData::ObjectWithIndex(_)
68            | TypeData::Array(_)
69            | TypeData::Tuple(_)
70            | TypeData::Mapped(_)
71            | TypeData::Function(_)
72            | TypeData::Callable(_)
73            | TypeData::Intrinsic(IntrinsicKind::Object | IntrinsicKind::Function),
74        ) => true,
75        Some(TypeData::ReadonlyType(inner)) => is_object_like_type_impl(types, inner),
76        Some(TypeData::Intersection(members)) => {
77            let members = types.type_list(members);
78            members
79                .iter()
80                .all(|&member| is_object_like_type_impl(types, member))
81        }
82        Some(TypeData::TypeParameter(info) | TypeData::Infer(info)) => info
83            .constraint
84            .is_some_and(|constraint| is_object_like_type_impl(types, constraint)),
85        _ => false,
86    }
87}
88
89/// Check if a type is an empty object type (no properties, no index signatures).
90pub fn is_empty_object_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
91    match types.lookup(type_id) {
92        Some(TypeData::Object(shape_id)) => {
93            let shape = types.object_shape(shape_id);
94            shape.properties.is_empty()
95        }
96        Some(TypeData::ObjectWithIndex(shape_id)) => {
97            let shape = types.object_shape(shape_id);
98            shape.properties.is_empty()
99                && shape.string_index.is_none()
100                && shape.number_index.is_none()
101        }
102        _ => false,
103    }
104}
105
106/// Check if a type is a primitive type (intrinsic or literal).
107pub fn is_primitive_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
108    // Check well-known intrinsic TypeIds first
109    if type_id.is_intrinsic() {
110        return true;
111    }
112    matches!(
113        types.lookup(type_id),
114        Some(TypeData::Intrinsic(_) | TypeData::Literal(_))
115    )
116}
117
118/// Check if a type is a union type.
119pub fn is_union_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
120    matches!(types.lookup(type_id), Some(TypeData::Union(_)))
121}
122
123/// Check if a type is an intersection type.
124pub fn is_intersection_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
125    matches!(types.lookup(type_id), Some(TypeData::Intersection(_)))
126}
127
128/// Check if a type is an array type.
129pub fn is_array_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
130    matches!(types.lookup(type_id), Some(TypeData::Array(_)))
131}
132
133/// Check if a type is a tuple type.
134pub fn is_tuple_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
135    matches!(types.lookup(type_id), Some(TypeData::Tuple(_)))
136}
137
138/// Check if a type is a type parameter.
139pub fn is_type_parameter(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
140    matches!(
141        types.lookup(type_id),
142        Some(TypeData::TypeParameter(_) | TypeData::Infer(_))
143    )
144}
145
146/// Check if a type is a conditional type.
147pub fn is_conditional_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
148    matches!(types.lookup(type_id), Some(TypeData::Conditional(_)))
149}
150
151/// Check if a type is a mapped type.
152pub fn is_mapped_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
153    matches!(types.lookup(type_id), Some(TypeData::Mapped(_)))
154}
155
156/// Check if a type is an index access type.
157pub fn is_index_access_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
158    matches!(types.lookup(type_id), Some(TypeData::IndexAccess(_, _)))
159}
160
161/// Check if a type is a template literal type.
162pub fn is_template_literal_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
163    matches!(types.lookup(type_id), Some(TypeData::TemplateLiteral(_)))
164}
165
166/// Check if a type is a type reference (Lazy/DefId).
167pub fn is_type_reference(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
168    matches!(
169        types.lookup(type_id),
170        Some(TypeData::Lazy(_) | TypeData::Recursive(_))
171    )
172}
173
174/// Check if a type is a generic type application.
175pub fn is_generic_application(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
176    matches!(types.lookup(type_id), Some(TypeData::Application(_)))
177}
178
179/// Check if a type can be compared by `TypeId` identity alone (O(1) equality).
180///
181/// Identity-comparable types are types where subtyping reduces to identity: two different
182/// identity-comparable types are always disjoint (neither is a subtype of the other).
183///
184/// This is used as an optimization to skip structural recursion in subtype checking.
185/// For example, comparing `[E.A, E.B]` vs `[E.C, E.D]` can return `source == target`
186/// in O(1) instead of walking into each tuple element.
187///
188/// Identity-comparable types include:
189/// - Literal types (string, number, boolean, bigint literals)
190/// - Enum members (`TypeData::Enum`)
191/// - Unique symbols
192/// - null, undefined, void, never
193/// - Tuples where ALL elements are identity-comparable (and no rest elements)
194///
195/// NOTE: This is NOT the same as tsc's `isUnitType` (which excludes void, never, and tuples).
196/// For tsc-compatible unit type semantics, use `type_queries::is_unit_type`.
197///
198/// NOTE: This does NOT handle `ReadonlyType` - readonly tuples must be checked separately
199/// because `["a"]` is a subtype of `readonly ["a"]` even though they have different `TypeIds`.
200pub fn is_identity_comparable_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
201    is_identity_comparable_type_impl(types, type_id, 0)
202}
203
204const MAX_IDENTITY_COMPARABLE_DEPTH: u32 = 10;
205
206fn is_identity_comparable_type_impl(types: &dyn TypeDatabase, type_id: TypeId, depth: u32) -> bool {
207    // Prevent stack overflow on pathological types
208    if depth > MAX_IDENTITY_COMPARABLE_DEPTH {
209        return false;
210    }
211
212    // Check well-known singleton types first
213    if type_id == TypeId::NULL
214        || type_id == TypeId::UNDEFINED
215        || type_id == TypeId::VOID
216        || type_id == TypeId::NEVER
217    {
218        return true;
219    }
220
221    match types.lookup(type_id) {
222        // Identity-comparable scalar types.
223        Some(TypeData::Literal(_))
224        | Some(TypeData::Enum(_, _))
225        | Some(TypeData::UniqueSymbol(_)) => true,
226
227        // Tuples are identity-comparable if ALL elements are (no rest elements)
228        Some(TypeData::Tuple(list_id)) => {
229            let elements = types.tuple_list(list_id);
230            // Check for rest elements - if any, not identity-comparable
231            if elements.iter().any(|e| e.rest) {
232                return false;
233            }
234            // All elements must be identity-comparable
235            elements
236                .iter()
237                .all(|e| is_identity_comparable_type_impl(types, e.type_id, depth + 1))
238        }
239
240        // Everything else is not identity-comparable
241        // ReadonlyType of an identity-comparable tuple is NOT considered identity-comparable
242        // because ["a"] <: readonly ["a"] but they have different TypeIds.
243        _ => false,
244    }
245}
246
247// =============================================================================
248// Type Contains Visitor - Check if a type contains specific types
249// =============================================================================
250
251/// Check if a type contains any type parameters.
252pub fn contains_type_parameters(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
253    contains_type_matching(types, type_id, |key| {
254        matches!(key, TypeData::TypeParameter(_) | TypeData::Infer(_))
255    })
256}
257
258/// Check if a type contains any `infer` types.
259pub fn contains_infer_types(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
260    contains_type_matching(types, type_id, |key| matches!(key, TypeData::Infer(_)))
261}
262
263/// Check if a type contains the error type.
264pub fn contains_error_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
265    if type_id == TypeId::ERROR {
266        return true;
267    }
268    contains_type_matching(types, type_id, |key| matches!(key, TypeData::Error))
269}
270
271/// Check if a type contains the `this` type anywhere.
272pub fn contains_this_type(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
273    contains_type_matching(types, type_id, |key| matches!(key, TypeData::ThisType))
274}
275
276/// Check if a type contains any type matching a predicate.
277pub fn contains_type_matching<F>(types: &dyn TypeDatabase, type_id: TypeId, predicate: F) -> bool
278where
279    F: Fn(&TypeData) -> bool,
280{
281    let mut checker = ContainsTypeChecker {
282        types,
283        predicate,
284        memo: FxHashMap::default(),
285        guard: crate::recursion::RecursionGuard::with_profile(
286            crate::recursion::RecursionProfile::ShallowTraversal,
287        ),
288    };
289    checker.check(type_id)
290}
291
292struct ContainsTypeChecker<'a, F>
293where
294    F: Fn(&TypeData) -> bool,
295{
296    types: &'a dyn TypeDatabase,
297    predicate: F,
298    memo: FxHashMap<TypeId, bool>,
299    guard: crate::recursion::RecursionGuard<TypeId>,
300}
301
302impl<'a, F> ContainsTypeChecker<'a, F>
303where
304    F: Fn(&TypeData) -> bool,
305{
306    fn check(&mut self, type_id: TypeId) -> bool {
307        if let Some(&cached) = self.memo.get(&type_id) {
308            return cached;
309        }
310
311        match self.guard.enter(type_id) {
312            crate::recursion::RecursionResult::Entered => {}
313            _ => return false,
314        }
315
316        let Some(key) = self.types.lookup(type_id) else {
317            self.guard.leave(type_id);
318            return false;
319        };
320
321        if (self.predicate)(&key) {
322            self.guard.leave(type_id);
323            self.memo.insert(type_id, true);
324            return true;
325        }
326
327        let result = self.check_key(&key);
328
329        self.guard.leave(type_id);
330        self.memo.insert(type_id, result);
331
332        result
333    }
334
335    fn check_key(&mut self, key: &TypeData) -> bool {
336        match key {
337            TypeData::Intrinsic(_)
338            | TypeData::Literal(_)
339            | TypeData::Error
340            | TypeData::ThisType
341            | TypeData::BoundParameter(_)
342            | TypeData::Lazy(_)
343            | TypeData::Recursive(_)
344            | TypeData::TypeQuery(_)
345            | TypeData::UniqueSymbol(_)
346            | TypeData::ModuleNamespace(_) => false,
347            TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id) => {
348                let shape = self.types.object_shape(*shape_id);
349                shape.properties.iter().any(|p| self.check(p.type_id))
350                    || shape
351                        .string_index
352                        .as_ref()
353                        .is_some_and(|i| self.check(i.value_type))
354                    || shape
355                        .number_index
356                        .as_ref()
357                        .is_some_and(|i| self.check(i.value_type))
358            }
359            TypeData::Union(list_id) | TypeData::Intersection(list_id) => {
360                let members = self.types.type_list(*list_id);
361                members.iter().any(|&m| self.check(m))
362            }
363            TypeData::Array(elem) => self.check(*elem),
364            TypeData::Tuple(list_id) => {
365                let elements = self.types.tuple_list(*list_id);
366                elements.iter().any(|e| self.check(e.type_id))
367            }
368            TypeData::Function(shape_id) => {
369                let shape = self.types.function_shape(*shape_id);
370                shape.params.iter().any(|p| self.check(p.type_id))
371                    || self.check(shape.return_type)
372                    || shape.this_type.is_some_and(|t| self.check(t))
373            }
374            TypeData::Callable(shape_id) => {
375                let shape = self.types.callable_shape(*shape_id);
376                shape.call_signatures.iter().any(|s| {
377                    s.params.iter().any(|p| self.check(p.type_id)) || self.check(s.return_type)
378                }) || shape.construct_signatures.iter().any(|s| {
379                    s.params.iter().any(|p| self.check(p.type_id)) || self.check(s.return_type)
380                }) || shape.properties.iter().any(|p| self.check(p.type_id))
381            }
382            TypeData::TypeParameter(info) | TypeData::Infer(info) => {
383                info.constraint.is_some_and(|c| self.check(c))
384                    || info.default.is_some_and(|d| self.check(d))
385            }
386            TypeData::Application(app_id) => {
387                let app = self.types.type_application(*app_id);
388                self.check(app.base) || app.args.iter().any(|&a| self.check(a))
389            }
390            TypeData::Conditional(cond_id) => {
391                let cond = self.types.conditional_type(*cond_id);
392                self.check(cond.check_type)
393                    || self.check(cond.extends_type)
394                    || self.check(cond.true_type)
395                    || self.check(cond.false_type)
396            }
397            TypeData::Mapped(mapped_id) => {
398                let mapped = self.types.mapped_type(*mapped_id);
399                mapped.type_param.constraint.is_some_and(|c| self.check(c))
400                    || mapped.type_param.default.is_some_and(|d| self.check(d))
401                    || self.check(mapped.constraint)
402                    || self.check(mapped.template)
403                    || mapped.name_type.is_some_and(|n| self.check(n))
404            }
405            TypeData::IndexAccess(obj, idx) => self.check(*obj) || self.check(*idx),
406            TypeData::TemplateLiteral(list_id) => {
407                let spans = self.types.template_list(*list_id);
408                spans.iter().any(|span| {
409                    if let crate::types::TemplateSpan::Type(type_id) = span {
410                        self.check(*type_id)
411                    } else {
412                        false
413                    }
414                })
415            }
416            TypeData::KeyOf(inner) | TypeData::ReadonlyType(inner) | TypeData::NoInfer(inner) => {
417                self.check(*inner)
418            }
419            TypeData::StringIntrinsic { type_arg, .. } => self.check(*type_arg),
420            TypeData::Enum(_def_id, member_type) => self.check(*member_type),
421        }
422    }
423}
424
425// =============================================================================
426// TypeDatabase-based convenience functions
427// =============================================================================
428
429/// Check if a type is a literal type (`TypeDatabase` version).
430pub fn is_literal_type_db(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
431    LiteralTypeChecker::check(types, type_id)
432}
433
434/// Check if a type is a function type (`TypeDatabase` version).
435pub fn is_function_type_db(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
436    FunctionTypeChecker::check(types, type_id)
437}
438
439/// Check if a type is object-like (`TypeDatabase` version).
440pub fn is_object_like_type_db(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
441    ObjectTypeChecker::check(types, type_id)
442}
443
444/// Check if a type is an empty object type (`TypeDatabase` version).
445pub fn is_empty_object_type_db(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
446    let checker = EmptyObjectChecker::new(types);
447    checker.check(type_id)
448}
449
450// =============================================================================
451// Object Type Classification
452// =============================================================================
453
454/// Classification of object types for freshness tracking.
455pub enum ObjectTypeKind {
456    /// A regular object type (no index signatures).
457    Object(ObjectShapeId),
458    /// An object type with index signatures.
459    ObjectWithIndex(ObjectShapeId),
460    /// Not an object type.
461    NotObject,
462}
463
464/// Classify a type as an object type kind.
465///
466/// This is used by the freshness tracking system to determine if a type
467/// is a fresh object literal that needs special handling.
468pub fn classify_object_type(types: &dyn TypeDatabase, type_id: TypeId) -> ObjectTypeKind {
469    match types.lookup(type_id) {
470        Some(TypeData::Object(shape_id)) => ObjectTypeKind::Object(shape_id),
471        Some(TypeData::ObjectWithIndex(shape_id)) => ObjectTypeKind::ObjectWithIndex(shape_id),
472        _ => ObjectTypeKind::NotObject,
473    }
474}
475
476// =============================================================================
477// Visitor Pattern Implementations for Helper Functions
478// =============================================================================
479
480/// Visitor to check if a type is a literal type.
481struct LiteralTypeChecker;
482
483impl LiteralTypeChecker {
484    fn check(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
485        match types.lookup(type_id) {
486            Some(TypeData::Literal(_)) => true,
487            Some(TypeData::ReadonlyType(inner) | TypeData::NoInfer(inner)) => {
488                Self::check(types, inner)
489            }
490            Some(TypeData::TypeParameter(info) | TypeData::Infer(info)) => {
491                info.constraint.is_some_and(|c| Self::check(types, c))
492            }
493            _ => false,
494        }
495    }
496}
497
498/// Visitor to check if a type is a function type.
499struct FunctionTypeChecker;
500
501impl FunctionTypeChecker {
502    fn check(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
503        match types.lookup(type_id) {
504            Some(TypeData::Function(_) | TypeData::Callable(_)) => true,
505            Some(TypeData::Intersection(members)) => {
506                let members = types.type_list(members);
507                members.iter().any(|&member| Self::check(types, member))
508            }
509            Some(TypeData::TypeParameter(info) | TypeData::Infer(info)) => {
510                info.constraint.is_some_and(|c| Self::check(types, c))
511            }
512            _ => false,
513        }
514    }
515}
516
517/// Visitor to check if a type is object-like.
518struct ObjectTypeChecker;
519
520impl ObjectTypeChecker {
521    fn check(types: &dyn TypeDatabase, type_id: TypeId) -> bool {
522        match types.lookup(type_id) {
523            Some(
524                TypeData::Object(_)
525                | TypeData::ObjectWithIndex(_)
526                | TypeData::Array(_)
527                | TypeData::Tuple(_)
528                | TypeData::Mapped(_),
529            ) => true,
530            Some(TypeData::ReadonlyType(inner) | TypeData::NoInfer(inner)) => {
531                Self::check(types, inner)
532            }
533            Some(TypeData::Intersection(members)) => {
534                let members = types.type_list(members);
535                members.iter().all(|&member| Self::check(types, member))
536            }
537            Some(TypeData::TypeParameter(info) | TypeData::Infer(info)) => info
538                .constraint
539                .is_some_and(|constraint| Self::check(types, constraint)),
540            _ => false,
541        }
542    }
543}
544
545/// Visitor to check if a type is an empty object type.
546struct EmptyObjectChecker<'a> {
547    db: &'a dyn TypeDatabase,
548}
549
550impl<'a> EmptyObjectChecker<'a> {
551    fn new(db: &'a dyn TypeDatabase) -> Self {
552        Self { db }
553    }
554
555    fn check(&self, type_id: TypeId) -> bool {
556        match self.db.lookup(type_id) {
557            Some(TypeData::Object(shape_id)) => {
558                let shape = self.db.object_shape(shape_id);
559                shape.properties.is_empty()
560            }
561            Some(TypeData::ObjectWithIndex(shape_id)) => {
562                let shape = self.db.object_shape(shape_id);
563                shape.properties.is_empty()
564                    && shape.string_index.is_none()
565                    && shape.number_index.is_none()
566            }
567            Some(TypeData::ReadonlyType(inner) | TypeData::NoInfer(inner)) => self.check(inner),
568            Some(TypeData::TypeParameter(info) | TypeData::Infer(info)) => {
569                info.constraint.is_some_and(|c| self.check(c))
570            }
571            _ => false,
572        }
573    }
574}