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}