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