Skip to main content

tsz_solver/type_queries/
flow.rs

1//! Control Flow and Advanced Type Classification Queries
2//!
3//! This module provides classification helpers for control flow analysis
4//! (narrowing, type predicates, constructor instances) and advanced type queries
5//! (promise detection, comparability, contextual type parameter extraction).
6
7use crate::TypeDatabase;
8use crate::type_queries::{
9    StringLiteralKeyKind, classify_for_string_literal_keys, get_string_literal_value,
10    get_union_members, is_invokable_type,
11};
12use crate::types::{TypeData, TypeId};
13use tsz_common::Atom;
14
15// =============================================================================
16// Control Flow Type Classification Helpers
17// =============================================================================
18
19/// Classification for type predicate signature extraction.
20/// Used by control flow analysis to extract predicate signatures from callable types.
21#[derive(Debug, Clone)]
22pub enum PredicateSignatureKind {
23    /// Function type - has `type_predicate` and params in function shape
24    Function(crate::types::FunctionShapeId),
25    /// Callable type - check `call_signatures` for predicate
26    Callable(crate::types::CallableShapeId),
27    /// Union - search members for predicate
28    Union(Vec<TypeId>),
29    /// Intersection - search members for predicate
30    Intersection(Vec<TypeId>),
31    /// No predicate available
32    None,
33}
34
35/// Classify a type for predicate signature extraction.
36pub fn classify_for_predicate_signature(
37    db: &dyn TypeDatabase,
38    type_id: TypeId,
39) -> PredicateSignatureKind {
40    let Some(key) = db.lookup(type_id) else {
41        return PredicateSignatureKind::None;
42    };
43
44    match key {
45        TypeData::Function(shape_id) => PredicateSignatureKind::Function(shape_id),
46        TypeData::Callable(shape_id) => PredicateSignatureKind::Callable(shape_id),
47        TypeData::Union(members_id) => {
48            let members = db.type_list(members_id);
49            PredicateSignatureKind::Union(members.to_vec())
50        }
51        TypeData::Intersection(members_id) => {
52            let members = db.type_list(members_id);
53            PredicateSignatureKind::Intersection(members.to_vec())
54        }
55        _ => PredicateSignatureKind::None,
56    }
57}
58
59/// Classification for constructor instance type extraction.
60/// Used by instanceof narrowing to get the instance type from a constructor.
61#[derive(Debug, Clone)]
62pub enum ConstructorInstanceKind {
63    /// Callable type with construct signatures
64    Callable(crate::types::CallableShapeId),
65    /// Union - search members for construct signatures
66    Union(Vec<TypeId>),
67    /// Intersection - search members for construct signatures
68    Intersection(Vec<TypeId>),
69    /// Not a constructor type
70    None,
71}
72
73/// Classify a type for constructor instance type extraction.
74pub fn classify_for_constructor_instance(
75    db: &dyn TypeDatabase,
76    type_id: TypeId,
77) -> ConstructorInstanceKind {
78    let Some(key) = db.lookup(type_id) else {
79        return ConstructorInstanceKind::None;
80    };
81
82    match key {
83        TypeData::Callable(shape_id) => ConstructorInstanceKind::Callable(shape_id),
84        TypeData::Union(members_id) => {
85            let members = db.type_list(members_id);
86            ConstructorInstanceKind::Union(members.to_vec())
87        }
88        TypeData::Intersection(members_id) => {
89            let members = db.type_list(members_id);
90            ConstructorInstanceKind::Intersection(members.to_vec())
91        }
92        _ => ConstructorInstanceKind::None,
93    }
94}
95
96/// Extract the instance type from a constructor type.
97///
98/// Given a type with construct signatures, returns the union of their return types.
99/// Recursively handles union types (collecting from all members) and intersection types
100/// (returning from the first member with construct signatures).
101pub fn instance_type_from_constructor(db: &dyn TypeDatabase, type_id: TypeId) -> Option<TypeId> {
102    if type_id == TypeId::ANY || type_id == TypeId::UNKNOWN {
103        return Some(type_id);
104    }
105
106    match classify_for_constructor_instance(db, type_id) {
107        ConstructorInstanceKind::Callable(shape_id) => {
108            let shape = db.callable_shape(shape_id);
109            if shape.construct_signatures.is_empty() {
110                return None;
111            }
112            let returns: Vec<TypeId> = shape
113                .construct_signatures
114                .iter()
115                .map(|s| s.return_type)
116                .collect();
117            Some(if returns.len() == 1 {
118                returns[0]
119            } else {
120                db.union(returns)
121            })
122        }
123        ConstructorInstanceKind::Union(members) => {
124            let instance_types: Vec<TypeId> = members
125                .into_iter()
126                .filter_map(|m| instance_type_from_constructor(db, m))
127                .collect();
128            if instance_types.is_empty() {
129                None
130            } else if instance_types.len() == 1 {
131                Some(instance_types[0])
132            } else {
133                Some(db.union(instance_types))
134            }
135        }
136        ConstructorInstanceKind::Intersection(members) => {
137            // TypeScript takes the first member with construct signatures
138            members
139                .into_iter()
140                .find_map(|m| instance_type_from_constructor(db, m))
141        }
142        ConstructorInstanceKind::None => None,
143    }
144}
145
146/// Classification for type parameter constraint access.
147/// Used by narrowing to check if a type has a constraint to narrow.
148#[derive(Debug, Clone)]
149pub enum TypeParameterConstraintKind {
150    /// Type parameter with constraint
151    TypeParameter { constraint: Option<TypeId> },
152    /// Not a type parameter
153    None,
154}
155
156/// Classify a type to check if it's a type parameter with a constraint.
157pub fn classify_for_type_parameter_constraint(
158    db: &dyn TypeDatabase,
159    type_id: TypeId,
160) -> TypeParameterConstraintKind {
161    let Some(key) = db.lookup(type_id) else {
162        return TypeParameterConstraintKind::None;
163    };
164
165    match key {
166        TypeData::TypeParameter(info) | TypeData::Infer(info) => {
167            TypeParameterConstraintKind::TypeParameter {
168                constraint: info.constraint,
169            }
170        }
171        _ => TypeParameterConstraintKind::None,
172    }
173}
174
175/// Classification for union member access.
176/// Used by narrowing to filter union members.
177#[derive(Debug, Clone)]
178pub enum UnionMembersKind {
179    /// Union with members
180    Union(Vec<TypeId>),
181    /// Not a union
182    NotUnion,
183}
184
185/// Classify a type to check if it's a union and get its members.
186pub fn classify_for_union_members(db: &dyn TypeDatabase, type_id: TypeId) -> UnionMembersKind {
187    let Some(key) = db.lookup(type_id) else {
188        return UnionMembersKind::NotUnion;
189    };
190
191    match key {
192        TypeData::Union(members_id) => {
193            let members = db.type_list(members_id);
194            UnionMembersKind::Union(members.to_vec())
195        }
196        _ => UnionMembersKind::NotUnion,
197    }
198}
199
200/// Classification for checking if a type is definitely not an object.
201/// Used by instanceof and typeof narrowing.
202#[derive(Debug, Clone)]
203pub enum NonObjectKind {
204    /// Literal type (always non-object)
205    Literal,
206    /// Intrinsic primitive type (void, undefined, null, boolean, number, string, bigint, symbol, never)
207    IntrinsicPrimitive,
208    /// Object or potentially object type
209    MaybeObject,
210}
211
212/// Classify a type to check if it's definitely not an object.
213pub fn classify_for_non_object(db: &dyn TypeDatabase, type_id: TypeId) -> NonObjectKind {
214    let Some(key) = db.lookup(type_id) else {
215        return NonObjectKind::MaybeObject;
216    };
217
218    match key {
219        TypeData::Literal(_) => NonObjectKind::Literal,
220        TypeData::Intrinsic(kind) => {
221            use crate::IntrinsicKind;
222
223            match kind {
224                IntrinsicKind::Void
225                | IntrinsicKind::Undefined
226                | IntrinsicKind::Null
227                | IntrinsicKind::Boolean
228                | IntrinsicKind::Number
229                | IntrinsicKind::String
230                | IntrinsicKind::Bigint
231                | IntrinsicKind::Symbol
232                | IntrinsicKind::Never => NonObjectKind::IntrinsicPrimitive,
233                _ => NonObjectKind::MaybeObject,
234            }
235        }
236        _ => NonObjectKind::MaybeObject,
237    }
238}
239
240/// Classification for property presence checking.
241/// Used by 'in' operator narrowing.
242#[derive(Debug, Clone)]
243pub enum PropertyPresenceKind {
244    /// Intrinsic object type (unknown properties)
245    IntrinsicObject,
246    /// Object with shape - check properties
247    Object(crate::types::ObjectShapeId),
248    /// Callable with properties
249    Callable(crate::types::CallableShapeId),
250    /// Array or Tuple - numeric access
251    ArrayLike,
252    /// Unknown property presence
253    Unknown,
254}
255
256/// Classify a type for property presence checking.
257pub fn classify_for_property_presence(
258    db: &dyn TypeDatabase,
259    type_id: TypeId,
260) -> PropertyPresenceKind {
261    let Some(key) = db.lookup(type_id) else {
262        return PropertyPresenceKind::Unknown;
263    };
264
265    match key {
266        TypeData::Intrinsic(crate::IntrinsicKind::Object) => PropertyPresenceKind::IntrinsicObject,
267        TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id) => {
268            PropertyPresenceKind::Object(shape_id)
269        }
270        TypeData::Callable(shape_id) => PropertyPresenceKind::Callable(shape_id),
271        TypeData::Array(_) | TypeData::Tuple(_) => PropertyPresenceKind::ArrayLike,
272        _ => PropertyPresenceKind::Unknown,
273    }
274}
275
276/// Classification for falsy component extraction.
277/// Used by truthiness narrowing.
278#[derive(Debug, Clone)]
279pub enum FalsyComponentKind {
280    /// Literal type - check if falsy value
281    Literal(crate::LiteralValue),
282    /// Union - get falsy component from each member
283    Union(Vec<TypeId>),
284    /// Type parameter or infer - keep as is
285    TypeParameter,
286    /// Other types - no falsy component
287    None,
288}
289
290/// Classify a type for falsy component extraction.
291pub fn classify_for_falsy_component(db: &dyn TypeDatabase, type_id: TypeId) -> FalsyComponentKind {
292    let Some(key) = db.lookup(type_id) else {
293        return FalsyComponentKind::None;
294    };
295
296    match key {
297        TypeData::Literal(literal) => FalsyComponentKind::Literal(literal),
298        TypeData::Union(members_id) => {
299            let members = db.type_list(members_id);
300            FalsyComponentKind::Union(members.to_vec())
301        }
302        TypeData::TypeParameter(_) | TypeData::Infer(_) => FalsyComponentKind::TypeParameter,
303        _ => FalsyComponentKind::None,
304    }
305}
306
307/// Classification for literal value extraction.
308/// Used by element access and property access narrowing.
309#[derive(Debug, Clone)]
310pub enum LiteralValueKind {
311    /// String literal
312    String(tsz_common::interner::Atom),
313    /// Number literal
314    Number(f64),
315    /// Not a literal
316    None,
317}
318
319/// Classify a type to extract literal value (string or number).
320pub fn classify_for_literal_value(db: &dyn TypeDatabase, type_id: TypeId) -> LiteralValueKind {
321    let Some(key) = db.lookup(type_id) else {
322        return LiteralValueKind::None;
323    };
324
325    match key {
326        TypeData::Literal(crate::LiteralValue::String(atom)) => LiteralValueKind::String(atom),
327        TypeData::Literal(crate::LiteralValue::Number(num)) => LiteralValueKind::Number(num.0),
328        _ => LiteralValueKind::None,
329    }
330}
331
332/// Check if a type is suitable as a narrowing literal value.
333///
334/// Returns `Some(type_id)` for types that can be used as the comparand in
335/// discriminant or literal equality narrowing:
336/// - Literal types (string, number, boolean, bigint)
337/// - Enum member types (nominal enum values like `Types.Str`)
338///
339/// Returns `None` for all other types.
340pub fn is_narrowing_literal(db: &dyn TypeDatabase, type_id: TypeId) -> Option<TypeId> {
341    // null and undefined are unit types that can serve as discriminants
342    if type_id == TypeId::NULL || type_id == TypeId::UNDEFINED {
343        return Some(type_id);
344    }
345    let key = db.lookup(type_id)?;
346    match key {
347        TypeData::Literal(_) | TypeData::Enum(_, _) => Some(type_id),
348        _ => None,
349    }
350}
351
352/// Check if a type is a "unit type" — a type with exactly one inhabitant.
353///
354/// Matches tsc's `isUnitType`: `TypeFlags.Unit = Enum | Literal | UniqueESSymbol | Nullable`.
355/// Unit types: null, undefined, true, false, string/number/bigint literals, enum members,
356/// unique symbols. A union is a unit type if ALL its members are unit types.
357///
358/// NOTE: This intentionally excludes `void` and `never` to match tsc semantics.
359/// For solver-internal identity optimization (which includes void/never/tuples),
360/// use `is_identity_comparable_type` from `visitor_predicates`.
361pub fn is_unit_type(db: &dyn TypeDatabase, type_id: TypeId) -> bool {
362    if type_id == TypeId::NULL
363        || type_id == TypeId::UNDEFINED
364        || type_id == TypeId::BOOLEAN_TRUE
365        || type_id == TypeId::BOOLEAN_FALSE
366    {
367        return true;
368    }
369
370    match db.lookup(type_id) {
371        Some(TypeData::Literal(_))
372        | Some(TypeData::Enum(_, _))
373        | Some(TypeData::UniqueSymbol(_)) => true,
374        Some(TypeData::Union(list_id)) => {
375            let members = db.type_list(list_id);
376            members.iter().all(|&m| is_unit_type(db, m))
377        }
378        _ => false,
379    }
380}
381
382/// Check if a union type contains a specific member type.
383pub fn union_contains(db: &dyn TypeDatabase, type_id: TypeId, target: TypeId) -> bool {
384    if let Some(members) = get_union_members(db, type_id) {
385        members.contains(&target)
386    } else {
387        false
388    }
389}
390
391/// Check if a type is or contains `undefined` (directly or as a union member).
392pub fn type_includes_undefined(db: &dyn TypeDatabase, type_id: TypeId) -> bool {
393    type_id == TypeId::UNDEFINED || union_contains(db, type_id, TypeId::UNDEFINED)
394}
395
396/// Extract string literal key names from a type (single literal, or union of literals).
397///
398/// Returns an empty Vec if the type doesn't contain string literals.
399pub fn extract_string_literal_keys(
400    db: &dyn TypeDatabase,
401    type_id: TypeId,
402) -> Vec<tsz_common::interner::Atom> {
403    match classify_for_string_literal_keys(db, type_id) {
404        StringLiteralKeyKind::SingleString(name) => vec![name],
405        StringLiteralKeyKind::Union(members) => members
406            .iter()
407            .filter_map(|&member| get_string_literal_value(db, member))
408            .collect(),
409        StringLiteralKeyKind::NotStringLiteral => Vec::new(),
410    }
411}
412
413/// Extracts the return type from a callable type for declaration emit.
414///
415/// For overloaded functions (Callable), returns the return type of the first signature.
416/// For intersections, finds the first callable member and extracts its return type.
417///
418/// # Examples
419///
420/// ```ignore
421/// let return_type = type_queries::get_return_type(&db, function_type_id);
422/// ```
423///
424/// # Arguments
425///
426/// * `db` - The type database/interner
427/// * `type_id` - The `TypeId` of a function or callable type
428///
429/// # Returns
430///
431/// * `Some(TypeId)` - The return type if this is a callable type
432/// * `None` - If this is not a callable type or `type_id` is unknown
433pub fn get_return_type(db: &dyn TypeDatabase, type_id: TypeId) -> Option<TypeId> {
434    match db.lookup(type_id) {
435        Some(TypeData::Function(shape_id)) => Some(db.function_shape(shape_id).return_type),
436        Some(TypeData::Callable(shape_id)) => {
437            let shape = db.callable_shape(shape_id);
438            // For overloads, use the first signature's return type
439            shape.call_signatures.first().map(|sig| sig.return_type)
440        }
441        Some(TypeData::Intersection(list_id)) => {
442            // In an intersection, find the first callable member
443            let members = db.type_list(list_id);
444            members.iter().find_map(|&m| get_return_type(db, m))
445        }
446        _ => {
447            // Handle special intrinsic types
448            if type_id == TypeId::ANY {
449                Some(TypeId::ANY)
450            } else if type_id == TypeId::NEVER {
451                Some(TypeId::NEVER)
452            } else {
453                None
454            }
455        }
456    }
457}
458
459// =============================================================================
460// Promise and Iterable Type Queries
461// =============================================================================
462
463use crate::operations::property::PropertyAccessEvaluator;
464
465/// Check if a type is "promise-like" (has a callable 'then' method).
466///
467/// This is used to detect thenable types for async iterator handling.
468/// A type is promise-like if it has a 'then' property that is callable.
469///
470/// # Arguments
471///
472/// * `db` - The type database/interner
473/// * `resolver` - Type resolver for handling Lazy/Ref types
474/// * `type_id` - The type to check
475///
476/// # Returns
477///
478/// * `true` - If the type is promise-like (has callable 'then')
479/// * `false` - Otherwise
480///
481/// # Examples
482///
483/// ```ignore
484/// // Promise<T> is promise-like
485/// assert!(is_promise_like(&db, &resolver, promise_type));
486///
487/// // any is always promise-like
488/// assert!(is_promise_like(&db, &resolver, TypeId::ANY));
489///
490/// // Objects with 'then' method are promise-like
491/// // { then: (fn: (value: T) => void) => void }
492/// ```
493pub fn is_promise_like(db: &dyn crate::caches::db::QueryDatabase, type_id: TypeId) -> bool {
494    // The 'any' trap: any is always promise-like
495    if type_id == TypeId::ANY {
496        return true;
497    }
498
499    // Use PropertyAccessEvaluator to find 'then' property
500    // This handles Lazy/Ref/Intersection/Readonly correctly
501    let evaluator = PropertyAccessEvaluator::new(db);
502    evaluator
503        .resolve_property_access(type_id, "then")
504        .success_type()
505        .is_some_and(|then_type| {
506            // 'then' must be invokable (have call signatures) to be "thenable"
507            // A class with only construct signatures is not thenable
508            is_invokable_type(db, then_type)
509        })
510}
511
512/// Check if a type is a valid target for for...in loops.
513///
514/// In TypeScript, for...in loops work on object types, arrays, and type parameters.
515/// This function validates that a type can be used in a for...in statement.
516///
517/// # Arguments
518///
519/// * `db` - The type database/interner
520/// * `type_id` - The type to check
521///
522/// # Returns
523///
524/// * `true` - If valid for for...in (Object, Array, `TypeParameter`, Any)
525/// * `false` - Otherwise
526///
527/// # Examples
528///
529/// ```ignore
530/// // Objects are valid
531/// assert!(is_valid_for_in_target(&db, object_type));
532///
533/// // Arrays are valid
534/// assert!(is_valid_for_in_target(&db, array_type));
535///
536/// // Type parameters are valid (generic constraints)
537/// assert!(is_valid_for_in_target(&db, type_param_type));
538///
539/// // Primitives (except any) are not valid
540/// assert!(!is_valid_for_in_target(&db, TypeId::STRING));
541/// ```
542pub fn is_valid_for_in_target(db: &dyn TypeDatabase, type_id: TypeId) -> bool {
543    // Any is always valid
544    if type_id == TypeId::ANY {
545        return true;
546    }
547
548    // Primitives are valid (they box to objects in JS for...in)
549    if type_id == TypeId::STRING || type_id == TypeId::NUMBER || type_id == TypeId::BOOLEAN {
550        return true;
551    }
552
553    use crate::types::IntrinsicKind;
554    match db.lookup(type_id) {
555        // Object types are valid (for...in iterates properties)
556        Some(TypeData::Object(_) | TypeData::ObjectWithIndex(_))
557        | Some(TypeData::Array(_))
558        | Some(TypeData::TypeParameter(_))
559        | Some(TypeData::Tuple(_))
560        | Some(TypeData::Literal(_)) => true,
561        // Unions are valid if all members are valid
562        Some(TypeData::Union(list_id)) => {
563            let members = db.type_list(list_id);
564            members.iter().all(|&m| is_valid_for_in_target(db, m))
565        }
566        // Intersections are valid if any member is valid
567        Some(TypeData::Intersection(list_id)) => {
568            let members = db.type_list(list_id);
569            members.iter().any(|&m| is_valid_for_in_target(db, m))
570        }
571        // Intrinsic primitives
572        Some(TypeData::Intrinsic(kind)) => matches!(
573            kind,
574            IntrinsicKind::String
575                | IntrinsicKind::Number
576                | IntrinsicKind::Boolean
577                | IntrinsicKind::Symbol
578        ),
579        // Everything else is not valid for for...in
580        _ => false,
581    }
582}
583
584/// Check if two types are "comparable" for TS2352 type assertion overlap check.
585///
586/// TSC uses `isTypeComparableTo` which is more relaxed than assignability.
587/// Types are comparable if:
588/// 1. They share at least one common object property name
589/// 2. One is a base primitive type and the other is a literal/union of that primitive
590/// 3. For union types, any member is comparable to the other type
591///
592/// This prevents false TS2352 errors on valid type assertions.
593pub fn types_are_comparable(db: &dyn TypeDatabase, source: TypeId, target: TypeId) -> bool {
594    types_are_comparable_inner(db, source, target, 0)
595}
596
597fn types_are_comparable_inner(
598    db: &dyn TypeDatabase,
599    source: TypeId,
600    target: TypeId,
601    depth: u32,
602) -> bool {
603    // Prevent infinite recursion
604    if depth > 5 {
605        return false;
606    }
607
608    // Same type is always comparable
609    if source == target {
610        return true;
611    }
612
613    // Type parameters are not automatically comparable for TS2352 purposes.
614    // Treating them as "comparable to anything" suppresses valid diagnostics
615    // like asserting a specific subtype to an unconcretized type parameter.
616
617    // Check union types: a union is comparable if ANY member is comparable
618    if let Some(TypeData::Union(list_id)) = db.lookup(source) {
619        let members = db.type_list(list_id);
620        return members
621            .iter()
622            .any(|&m| types_are_comparable_inner(db, m, target, depth + 1));
623    }
624    if let Some(TypeData::Union(list_id)) = db.lookup(target) {
625        let members = db.type_list(list_id);
626        return members
627            .iter()
628            .any(|&m| types_are_comparable_inner(db, source, m, depth + 1));
629    }
630
631    // Check primitive ↔ literal comparability
632    // string is comparable to any string literal
633    // number is comparable to any numeric literal
634    // etc.
635    if is_primitive_comparable(db, source, target) || is_primitive_comparable(db, target, source) {
636        return true;
637    }
638
639    // Check object property overlap
640    types_have_common_properties(db, source, target, depth)
641}
642
643/// Check if a base primitive type is comparable to a literal or other form of that primitive.
644fn is_primitive_comparable(db: &dyn TypeDatabase, base: TypeId, other: TypeId) -> bool {
645    // string is comparable to string literals
646    if base == TypeId::STRING {
647        if let Some(TypeData::Literal(lit)) = db.lookup(other) {
648            return matches!(lit, crate::types::LiteralValue::String(_));
649        }
650        return other == TypeId::STRING;
651    }
652    // number is comparable to numeric literals
653    if base == TypeId::NUMBER {
654        if let Some(TypeData::Literal(lit)) = db.lookup(other) {
655            return matches!(lit, crate::types::LiteralValue::Number(_));
656        }
657        return other == TypeId::NUMBER;
658    }
659    // boolean is comparable to true/false
660    if base == TypeId::BOOLEAN {
661        return other == TypeId::BOOLEAN_TRUE
662            || other == TypeId::BOOLEAN_FALSE
663            || other == TypeId::BOOLEAN;
664    }
665    // bigint is comparable to bigint literals
666    if base == TypeId::BIGINT {
667        if let Some(TypeData::Literal(lit)) = db.lookup(other) {
668            return matches!(lit, crate::types::LiteralValue::BigInt(_));
669        }
670        return other == TypeId::BIGINT;
671    }
672    // Two literals of the same primitive kind are comparable (e.g. "foo" ~ "baz",  1 ~ 2).
673    // In tsc, comparability checks the "base constraint" — both widen to the same primitive.
674    if let Some(TypeData::Literal(lit_a)) = db.lookup(base) {
675        if let Some(TypeData::Literal(lit_b)) = db.lookup(other) {
676            return std::mem::discriminant(&lit_a) == std::mem::discriminant(&lit_b);
677        }
678        // literal vs its base primitive: "foo" ~ string, 1 ~ number
679        return match lit_a {
680            crate::types::LiteralValue::String(_) => other == TypeId::STRING,
681            crate::types::LiteralValue::Number(_) => other == TypeId::NUMBER,
682            crate::types::LiteralValue::BigInt(_) => other == TypeId::BIGINT,
683            crate::types::LiteralValue::Boolean(_) => {
684                other == TypeId::BOOLEAN
685                    || other == TypeId::BOOLEAN_TRUE
686                    || other == TypeId::BOOLEAN_FALSE
687            }
688        };
689    }
690    // true/false are comparable to each other
691    if (base == TypeId::BOOLEAN_TRUE || base == TypeId::BOOLEAN_FALSE)
692        && (other == TypeId::BOOLEAN_TRUE || other == TypeId::BOOLEAN_FALSE)
693    {
694        return true;
695    }
696    false
697}
698
699/// Check if two types share at least one common property name.
700fn types_have_common_properties(
701    db: &dyn TypeDatabase,
702    source: TypeId,
703    target: TypeId,
704    depth: u32,
705) -> bool {
706    // Helper to get properties from an object/callable type
707    fn get_properties(db: &dyn TypeDatabase, type_id: TypeId) -> Vec<(Atom, TypeId)> {
708        match db.lookup(type_id) {
709            Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
710                let shape = db.object_shape(shape_id);
711                shape
712                    .properties
713                    .iter()
714                    .map(|p| (p.name, p.type_id))
715                    .collect()
716            }
717            Some(TypeData::Callable(callable_id)) => {
718                let shape = db.callable_shape(callable_id);
719                shape
720                    .properties
721                    .iter()
722                    .map(|p| (p.name, p.type_id))
723                    .collect()
724            }
725            Some(TypeData::Intersection(list_id)) => {
726                let members = db.type_list(list_id);
727                let mut props = Vec::new();
728                for &member in members.iter() {
729                    props.extend(get_properties(db, member));
730                }
731                props
732            }
733            _ => Vec::new(),
734        }
735    }
736
737    let source_props = get_properties(db, source);
738    let target_props = get_properties(db, target);
739
740    if source_props.is_empty() || target_props.is_empty() {
741        return false;
742    }
743
744    // Consider overlap only when a shared property has comparable types.
745    // Name-only matching is too permissive and suppresses valid TS2352 cases
746    // on incompatible generic instantiations.
747    use rustc_hash::FxHashMap;
748    let mut target_by_name: FxHashMap<Atom, Vec<TypeId>> = FxHashMap::default();
749    for (name, ty) in target_props {
750        target_by_name.entry(name).or_default().push(ty);
751    }
752
753    source_props.iter().any(|(source_name, source_ty)| {
754        target_by_name.get(source_name).is_some_and(|target_tys| {
755            target_tys
756                .iter()
757                .any(|target_ty| types_are_comparable_inner(db, *source_ty, *target_ty, depth + 1))
758        })
759    })
760}
761
762/// Check if a type contains a `TypeQuery` referencing a specific symbol.
763///
764/// Used for TS2502 detection (circular reference in type annotation).
765/// Traverses the type structure, expanding top-level lazy aliases via the provided callback.
766/// Stops recursion at Function, Object, and Mapped types which break the "direct" reference cycle.
767#[allow(clippy::match_same_arms)]
768pub fn has_type_query_for_symbol(
769    db: &dyn TypeDatabase,
770    type_id: TypeId,
771    target_sym_id: u32,
772    mut resolve_lazy: impl FnMut(TypeId) -> TypeId,
773) -> bool {
774    use crate::TypeData;
775    use rustc_hash::FxHashSet;
776
777    let mut worklist = vec![type_id];
778    let mut visited = FxHashSet::default();
779
780    while let Some(ty) = worklist.pop() {
781        if !visited.insert(ty) {
782            continue;
783        }
784
785        let resolved = resolve_lazy(ty);
786        if resolved != ty {
787            worklist.push(resolved);
788            continue;
789        }
790
791        let Some(key) = db.lookup(ty) else { continue };
792        match key {
793            TypeData::TypeQuery(sym_ref) => {
794                if sym_ref.0 == target_sym_id {
795                    return true;
796                }
797            }
798            TypeData::Array(elem) => worklist.push(elem),
799            TypeData::Union(list) | TypeData::Intersection(list) => {
800                let members = db.type_list(list);
801                worklist.extend(members.iter().copied());
802            }
803            TypeData::Tuple(list) => {
804                let elements = db.tuple_list(list);
805                for elem in elements.iter() {
806                    worklist.push(elem.type_id);
807                }
808            }
809            TypeData::Conditional(id) => {
810                let cond = db.conditional_type(id);
811                worklist.push(cond.check_type);
812                worklist.push(cond.extends_type);
813                worklist.push(cond.true_type);
814                worklist.push(cond.false_type);
815            }
816            TypeData::Application(id) => {
817                let app = db.type_application(id);
818                worklist.push(app.base);
819                worklist.extend(&app.args);
820            }
821            TypeData::IndexAccess(obj, idx) => {
822                worklist.push(obj);
823                worklist.push(idx);
824            }
825            TypeData::KeyOf(inner) | TypeData::ReadonlyType(inner) => {
826                worklist.push(inner);
827            }
828            TypeData::Function(_)
829            | TypeData::Object(_)
830            | TypeData::ObjectWithIndex(_)
831            | TypeData::Mapped(_) => {
832                // These types break the "direct" reference cycle logic for TS2502.
833                // Recursive types via function return/params or object properties are allowed.
834            }
835            _ => {}
836        }
837    }
838    false
839}
840
841/// Extract contextual type parameters from a type.
842///
843/// Inspects function shapes, callable shapes (single call signature),
844/// type applications (recurse into base), and unions (all members must agree).
845/// Returns `None` if the type has no extractable type parameters or if
846/// union members disagree.
847///
848/// This encapsulates the common checker pattern of extracting type parameters
849/// from an expected contextual type for generic function inference.
850pub fn extract_contextual_type_params(
851    db: &dyn TypeDatabase,
852    type_id: TypeId,
853) -> Option<Vec<crate::types::TypeParamInfo>> {
854    extract_contextual_type_params_inner(db, type_id, 0)
855}
856
857fn extract_contextual_type_params_inner(
858    db: &dyn TypeDatabase,
859    type_id: TypeId,
860    depth: u32,
861) -> Option<Vec<crate::types::TypeParamInfo>> {
862    if depth > 20 {
863        return None;
864    }
865
866    match db.lookup(type_id) {
867        Some(TypeData::Function(shape_id)) => {
868            let shape = db.function_shape(shape_id);
869            if shape.type_params.is_empty() {
870                None
871            } else {
872                Some(shape.type_params.clone())
873            }
874        }
875        Some(TypeData::Callable(shape_id)) => {
876            let shape = db.callable_shape(shape_id);
877            if shape.call_signatures.len() != 1 {
878                return None;
879            }
880            let sig = &shape.call_signatures[0];
881            if sig.type_params.is_empty() {
882                None
883            } else {
884                Some(sig.type_params.clone())
885            }
886        }
887        Some(TypeData::Application(app_id)) => {
888            let app = db.type_application(app_id);
889            extract_contextual_type_params_inner(db, app.base, depth + 1)
890        }
891        Some(TypeData::Union(list_id)) => {
892            let members = db.type_list(list_id);
893            if members.is_empty() {
894                return None;
895            }
896            let mut candidate: Option<Vec<crate::types::TypeParamInfo>> = None;
897            for &member in members.iter() {
898                let params = extract_contextual_type_params_inner(db, member, depth + 1)?;
899                if let Some(existing) = &candidate {
900                    if existing.len() != params.len()
901                        || existing
902                            .iter()
903                            .zip(params.iter())
904                            .any(|(left, right)| left != right)
905                    {
906                        return None;
907                    }
908                } else {
909                    candidate = Some(params);
910                }
911            }
912            candidate
913        }
914        _ => None,
915    }
916}