Skip to main content

tsz_solver/relations/
subtype_overlap.rs

1//! Type overlap detection for subtype checking.
2//!
3//! This module implements overlap detection between types, used for TS2367
4//! ("This condition will always return 'false' since the types 'X' and 'Y' have no overlap").
5
6use crate::relations::subtype::SubtypeChecker;
7use crate::type_resolver::TypeResolver;
8use crate::types::{IntrinsicKind, LiteralValue, TemplateLiteralId, TemplateSpan, TypeId};
9use crate::visitor::{intrinsic_kind, literal_value, template_literal_id};
10
11impl<'a, R: TypeResolver> SubtypeChecker<'a, R> {
12    /// Check if two types have any overlap (non-empty intersection).
13    ///
14    /// This is used for TS2367: "This condition will always return 'false' since the types 'X' and 'Y' have no overlap."
15    ///
16    /// Returns true if there exists at least one type that is a subtype of both a and b.
17    /// Returns false if a & b would be the `never` type (zero overlap).
18    ///
19    /// This catches OBVIOUS non-overlaps:
20    /// - Different primitives (string vs number, boolean vs bigint, etc.)
21    /// - Different literals of same primitive ("a" vs "b", 1 vs 2)
22    /// - Object property type mismatches ({ a: string } vs { a: number })
23    ///
24    /// For complex types (unions, intersections, generics), we conservatively return true
25    /// to avoid false positives.
26    ///
27    /// # Examples
28    /// - `are_types_overlapping(string, number)` -> false (different primitives)
29    /// - `are_types_overlapping(1, 2)` -> false (different number literals)
30    /// - `are_types_overlapping({ a: string }, { a: number })` -> false (property type mismatch)
31    /// - `are_types_overlapping({ a: 1 }, { b: 2 })` -> true (can have { a: 1, b: 2 })
32    /// - `are_types_overlapping(string, "hello")` -> true (literal is subtype of primitive)
33    pub fn are_types_overlapping(&self, a: TypeId, b: TypeId) -> bool {
34        // Fast path: identical types overlap (unless never)
35        if a == b {
36            return a != TypeId::NEVER;
37        }
38
39        // Top types: any/unknown overlap with everything except never
40        if a.is_any_or_unknown() {
41            return !b.is_never();
42        }
43        if b.is_any_or_unknown() {
44            return !a.is_never();
45        }
46
47        // Bottom type: never overlaps with nothing
48        if a == TypeId::NEVER || b == TypeId::NEVER {
49            return false;
50        }
51
52        // Resolve Lazy/Ref types before checking
53        let a_resolved = self.resolve_ref_type(a);
54        let b_resolved = self.resolve_ref_type(b);
55
56        // Special handling for TypeParameter and Infer
57        if let Some(
58            crate::types::TypeData::TypeParameter(info) | crate::types::TypeData::Infer(info),
59        ) = self.interner.lookup(a_resolved)
60        {
61            if let Some(constraint) = info.constraint {
62                return self.are_types_overlapping(constraint, b_resolved);
63            }
64            // Without constraint, it can overlap with anything
65            return true;
66        }
67
68        if let Some(
69            crate::types::TypeData::TypeParameter(info) | crate::types::TypeData::Infer(info),
70        ) = self.interner.lookup(b_resolved)
71        {
72            if let Some(constraint) = info.constraint {
73                return self.are_types_overlapping(a_resolved, constraint);
74            }
75            return true;
76        }
77
78        // Check if either is subtype of the other (sufficient condition, not necessary)
79        // This catches: literal <: primitive, object <: interface, etc.
80        // Note: check_subtype returns SubtypeResult, but we need &mut self for it
81        // For now, we'll use a simpler approach that doesn't require mutation
82        if self.are_types_in_subtype_relation(a_resolved, b_resolved) {
83            return true;
84        }
85
86        // Check for different primitive types
87        if let (Some(a_kind), Some(b_kind)) = (
88            intrinsic_kind(self.interner, a_resolved),
89            intrinsic_kind(self.interner, b_resolved),
90        ) {
91            // 1. Handle strictNullChecks
92            if !self.strict_null_checks {
93                // If strict null checks is OFF, null/undefined overlap with everything
94                if matches!(a_kind, IntrinsicKind::Null | IntrinsicKind::Undefined)
95                    || matches!(b_kind, IntrinsicKind::Null | IntrinsicKind::Undefined)
96                {
97                    return true;
98                }
99            }
100
101            // 2. Handle Void vs Undefined (always overlap)
102            if (a_kind == IntrinsicKind::Void && b_kind == IntrinsicKind::Undefined)
103                || (a_kind == IntrinsicKind::Undefined && b_kind == IntrinsicKind::Void)
104            {
105                return true;
106            }
107
108            // 3. Handle Null/Undefined comparisons (always allowed for TS2367 purposes)
109            // TypeScript allows null/undefined to be compared with ANY type without TS2367.
110            // This is true even with strict null checks enabled.
111            // Examples that should NOT emit TS2367:
112            //   - null !== undefined
113            //   - null == 5
114            //   - "hello" === undefined
115            // TS2367 is only for truly incompatible types like "hello" === 5 or 1 === "2".
116            if matches!(a_kind, IntrinsicKind::Null | IntrinsicKind::Undefined)
117                || matches!(b_kind, IntrinsicKind::Null | IntrinsicKind::Undefined)
118            {
119                return true;
120            }
121
122            // 4. Compare primitives
123            match (a_kind, b_kind) {
124                (IntrinsicKind::String, IntrinsicKind::String)
125                | (IntrinsicKind::Number, IntrinsicKind::Number)
126                | (IntrinsicKind::Boolean, IntrinsicKind::Boolean)
127                | (IntrinsicKind::Bigint, IntrinsicKind::Bigint)
128                | (IntrinsicKind::Symbol, IntrinsicKind::Symbol) => {
129                    // Same primitive type - check if they're different literals
130                    return self.are_literals_overlapping(a_resolved, b_resolved);
131                }
132                // Distinct primitives do not overlap
133                (
134                    IntrinsicKind::String
135                    | IntrinsicKind::Number
136                    | IntrinsicKind::Boolean
137                    | IntrinsicKind::Bigint
138                    | IntrinsicKind::Symbol
139                    | IntrinsicKind::Null
140                    | IntrinsicKind::Undefined
141                    | IntrinsicKind::Void
142                    | IntrinsicKind::Object,
143                    _,
144                ) => {
145                    return false;
146                }
147                // Handle Object keyword vs Primitives (Disjoint)
148                // Note: It DOES overlap with Object (interface), but that is handled
149                // by object_shape_id, not intrinsic_kind.
150                // Fallback for any new intrinsics added later
151                _ => return true,
152            }
153        }
154
155        // Check for different literal values of the same primitive type
156        if let (Some(a_lit), Some(b_lit)) = (
157            literal_value(self.interner, a_resolved),
158            literal_value(self.interner, b_resolved),
159        ) {
160            // Different literal values never overlap
161            return a_lit == b_lit;
162        }
163
164        // For object-like types, use refined overlap detection with PropertyCollector
165        // This handles: objects, objects with index signatures, and intersections
166        // This replaces the simplified check that only handled direct object-to-object
167        let is_a_obj = self.is_object_like(a_resolved);
168        let is_b_obj = self.is_object_like(b_resolved);
169
170        if is_a_obj && is_b_obj {
171            return self.do_refined_object_overlap_check(a_resolved, b_resolved);
172        }
173
174        // Template literal disjointness detection
175        // Two template literals with different starting/ending text are disjoint
176        if let (Some(a_spans), Some(b_spans)) = (
177            template_literal_id(self.interner, a_resolved),
178            template_literal_id(self.interner, b_resolved),
179        ) {
180            return self.are_template_literals_overlapping(a_spans, b_spans);
181        }
182
183        if let Some(crate::types::TypeData::Union(list_id)) = self.interner.lookup(a_resolved) {
184            return self
185                .interner
186                .type_list(list_id)
187                .iter()
188                .any(|&member| self.are_types_overlapping(member, b_resolved));
189        }
190
191        if let Some(crate::types::TypeData::Union(list_id)) = self.interner.lookup(b_resolved) {
192            return self
193                .interner
194                .type_list(list_id)
195                .iter()
196                .any(|&member| self.are_types_overlapping(a_resolved, member));
197        }
198
199        if let Some(crate::types::TypeData::Enum(_, underlying)) = self.interner.lookup(a_resolved)
200        {
201            return self.are_types_overlapping(underlying, b_resolved);
202        }
203
204        if let Some(crate::types::TypeData::Enum(_, underlying)) = self.interner.lookup(b_resolved)
205        {
206            return self.are_types_overlapping(a_resolved, underlying);
207        }
208
209        // Conservative: assume overlap for complex types we haven't fully handled yet
210        // (intersections, generics, etc.)
211        // Better to miss some TS2367 errors than to emit them incorrectly
212        true
213    }
214
215    /// Check if one type is a subtype of the other without mutation.
216    ///
217    /// This is a simplified version that checks obvious subtype relationships
218    /// without needing to call the full `check_subtype` which requires &mut self.
219    fn are_types_in_subtype_relation(&self, a: TypeId, b: TypeId) -> bool {
220        // Check identity first
221        if a == b {
222            return true;
223        }
224
225        // Check for literal-to-primitive relationships
226        if let (Some(a_lit), Some(b_kind)) = (
227            literal_value(self.interner, a),
228            intrinsic_kind(self.interner, b),
229        ) {
230            return matches!(
231                (a_lit, b_kind),
232                (LiteralValue::String(_), IntrinsicKind::String)
233                    | (LiteralValue::Number(_), IntrinsicKind::Number)
234                    | (LiteralValue::BigInt(_), IntrinsicKind::Bigint)
235                    | (LiteralValue::Boolean(_), IntrinsicKind::Boolean)
236            );
237        }
238
239        if let (Some(a_kind), Some(b_lit)) = (
240            intrinsic_kind(self.interner, a),
241            literal_value(self.interner, b),
242        ) {
243            return matches!(
244                (a_kind, b_lit),
245                (IntrinsicKind::String, LiteralValue::String(_))
246                    | (IntrinsicKind::Number, LiteralValue::Number(_))
247                    | (IntrinsicKind::Bigint, LiteralValue::BigInt(_))
248                    | (IntrinsicKind::Boolean, LiteralValue::Boolean(_))
249            );
250        }
251
252        false
253    }
254
255    /// Check if two literal types have overlapping values.
256    ///
257    /// Returns false if they're different literals of the same primitive type.
258    /// Returns true if they're the same literal or if we can't determine.
259    fn are_literals_overlapping(&self, a: TypeId, b: TypeId) -> bool {
260        if let (Some(a_lit), Some(b_lit)) = (
261            literal_value(self.interner, a),
262            literal_value(self.interner, b),
263        ) {
264            // Different literal values of the same primitive type never overlap
265            a_lit == b_lit
266        } else {
267            // At least one isn't a literal, so they overlap
268            true
269        }
270    }
271
272    /// Check if two template literal types have any overlap.
273    ///
274    /// Template literals are disjoint if they have incompatible fixed text spans.
275    /// For example:
276    /// - `foo${string}` and `bar${string}` are disjoint (different prefixes)
277    /// - `foo${string}` and `foo${number}` may overlap (same prefix, compatible types)
278    /// - `a${string}b` and `a${string}c` are disjoint (different suffixes)
279    ///
280    /// Returns false if types are guaranteed disjoint, true otherwise.
281    fn are_template_literals_overlapping(
282        &self,
283        a: TemplateLiteralId,
284        b: TemplateLiteralId,
285    ) -> bool {
286        // Fast path: same template literal definitely overlaps
287        if a == b {
288            return true;
289        }
290
291        let a_spans = self.interner.template_list(a);
292        let b_spans = self.interner.template_list(b);
293
294        // Templates with different numbers of spans might still overlap
295        // if the type holes are wide enough (e.g., string)
296        // We need to check if there's any possible string that matches both patterns
297
298        // For simplicity, we check if there are incompatible fixed text spans
299        let a_len = a_spans.len();
300        let b_len = b_spans.len();
301
302        // Collect fixed text patterns from both templates
303        // Two templates are disjoint if they have incompatible fixed text at any position
304        let mut a_idx = 0;
305        let mut b_idx = 0;
306
307        loop {
308            // Skip type holes in both templates
309            while a_idx < a_len && matches!(a_spans[a_idx], TemplateSpan::Type(_)) {
310                a_idx += 1;
311            }
312            while b_idx < b_len && matches!(b_spans[b_idx], TemplateSpan::Type(_)) {
313                b_idx += 1;
314            }
315
316            // If both reached the end, they overlap (both can match empty string after all type holes)
317            if a_idx >= a_len && b_idx >= b_len {
318                return true;
319            }
320
321            // If only one reached the end, check if the remaining can be empty
322            if a_idx >= a_len {
323                // A exhausted, B has more content
324                // They overlap only if B's remaining content is all type holes
325                return b_spans[b_idx..]
326                    .iter()
327                    .all(|s| matches!(s, TemplateSpan::Type(_)));
328            }
329            if b_idx >= b_len {
330                // B exhausted, A has more content
331                return a_spans[a_idx..]
332                    .iter()
333                    .all(|s| matches!(s, TemplateSpan::Type(_)));
334            }
335
336            // Both have text spans - check if they match
337            match (&a_spans[a_idx], &b_spans[b_idx]) {
338                (TemplateSpan::Text(a_text), TemplateSpan::Text(b_text)) => {
339                    let a_str = self.interner.resolve_atom(*a_text);
340                    let b_str = self.interner.resolve_atom(*b_text);
341
342                    // Check if the text spans can match
343                    // They must have at least one common prefix
344                    let min_len = a_str.len().min(b_str.len());
345                    if a_str[..min_len] != b_str[..min_len] {
346                        // Incompatible prefixes - templates are disjoint
347                        return false;
348                    }
349
350                    // Advance past the common prefix
351                    let advance = min_len;
352                    a_idx += 1;
353                    b_idx += 1;
354
355                    // If one text span is exhausted, the other must have type holes to continue
356                    if a_str.len() > advance {
357                        // A's text is longer - B needs a type hole to consume the rest
358                        if b_idx >= b_len || !matches!(b_spans[b_idx], TemplateSpan::Type(_)) {
359                            // B can't consume the rest of A's text - disjoint unless A's extra text is a prefix
360                            // that B's type hole can match
361                            return a_str[advance..].is_empty();
362                        }
363                    }
364                    if b_str.len() > advance {
365                        // B's text is longer - A needs a type hole to consume the rest
366                        if a_idx >= a_len || !matches!(a_spans[a_idx], TemplateSpan::Type(_)) {
367                            return b_str[advance..].is_empty();
368                        }
369                    }
370                }
371                _ => {
372                    // One is text, one is type - they're compatible
373                    // The type can match any string, so we advance both
374                    a_idx += 1;
375                    b_idx += 1;
376                }
377            }
378        }
379    }
380
381    /// Check if two types are "object-like" (should use `PropertyCollector` for overlap detection).
382    ///
383    /// Object-like types include:
384    /// - Plain objects with properties
385    /// - Objects with index signatures
386    /// - Intersections (which may contain objects)
387    fn is_object_like(&self, type_id: TypeId) -> bool {
388        use crate::visitor::{intersection_list_id, object_shape_id, object_with_index_shape_id};
389
390        object_shape_id(self.interner, type_id).is_some()
391            || object_with_index_shape_id(self.interner, type_id).is_some()
392            || intersection_list_id(self.interner, type_id).is_some()
393    }
394
395    /// Check if two object-like types have overlapping properties and index signatures.
396    ///
397    /// This is the refined implementation using `PropertyCollector` to handle:
398    /// - Intersections (flattened property collection)
399    /// - Index signatures (both string and number)
400    /// - Optional properties (correct undefined handling via `optional_property_type`)
401    /// - Discriminant detection (common property with disjoint literal types)
402    ///
403    /// Returns false if types have zero overlap, true otherwise.
404    fn do_refined_object_overlap_check(&self, a: TypeId, b: TypeId) -> bool {
405        use crate::objects::{PropertyCollectionResult, collect_properties};
406
407        // Collect properties and index signatures from both types
408        let res_a = collect_properties(a, self.interner, self.resolver);
409        let res_b = collect_properties(b, self.interner, self.resolver);
410
411        // Extract properties and index signatures from results
412        let (props_a, s_idx_a, _n_idx_a) = match res_a {
413            PropertyCollectionResult::Any | PropertyCollectionResult::NonObject => return true, // Any overlaps with everything
414            // Conservatively overlap
415            PropertyCollectionResult::Properties {
416                properties,
417                string_index,
418                number_index,
419            } => (properties, string_index, number_index),
420        };
421
422        let (props_b, s_idx_b, _n_idx_b) = match res_b {
423            PropertyCollectionResult::Any | PropertyCollectionResult::NonObject => return true,
424            PropertyCollectionResult::Properties {
425                properties,
426                string_index,
427                number_index,
428            } => (properties, string_index, number_index),
429        };
430
431        // 1. Check Common Properties for overlap
432        // If a property exists in both objects, their types must overlap
433        for p_a in &props_a {
434            if let Some(p_b) = props_b.iter().find(|p| p.name == p_a.name) {
435                // Use optional_property_type for correct undefined handling
436                let type_a = self.optional_property_type(p_a);
437                let type_b = self.optional_property_type(p_b);
438
439                if !self.are_types_overlapping(type_a, type_b) {
440                    return false; // Hard conflict - no overlap
441                }
442            }
443        }
444
445        // 2. Check Required Properties A against Index Signatures B
446        // Only REQUIRED properties must be compatible with B's string index.
447        // Optional properties can be missing (undefined) so they don't conflict with index signatures.
448        // Example: { a?: string } and { [k: string]: number } DO overlap because {} satisfies both.
449        if let Some(ref idx_b) = s_idx_b {
450            for p_a in &props_a {
451                if !p_a.optional {
452                    // Only check required properties
453                    if !self.are_types_overlapping(p_a.type_id, idx_b.value_type) {
454                        return false;
455                    }
456                }
457            }
458        }
459
460        // 3. Check Required Properties B against Index Signatures A
461        // Only REQUIRED properties must be compatible with A's string index.
462        if let Some(ref idx_a) = s_idx_a {
463            for p_b in &props_b {
464                if !p_b.optional {
465                    // Only check required properties
466                    if !self.are_types_overlapping(p_b.type_id, idx_a.value_type) {
467                        return false;
468                    }
469                }
470            }
471        }
472
473        // 4. Index Signature Compatibility Check
474        // NOTE: Index signatures do NOT prevent overlap even if their value types are disjoint
475        // because the empty object {} satisfies both index signatures.
476        // Example: { [k: string]: string } and { [k: string]: number } DO overlap.
477        // So NO CHECK needed here - index signatures never cause disjointness.
478
479        // All checks passed - types overlap
480        true
481    }
482}