Skip to main content

tsz_solver/relations/
compat_overrides.rs

1//! Nominal typing overrides for the compatibility checker.
2//!
3//! This module contains assignability override methods that implement
4//! TypeScript's nominal typing rules on top of the structural compatibility
5//! engine. These overrides handle:
6//!
7//! - **Enum nominality**: Different enums are not assignable even if structurally
8//!   identical. String enums are strictly nominal; numeric enums allow number
9//!   assignability.
10//! - **Private brand checking**: Classes with private/protected fields use nominal
11//!   typing — the `parent_id` on each property must match exactly.
12//! - **Redeclaration compatibility**: Variable redeclarations (`var x: T; var x: U`)
13//!   require nominal identity for enums and bidirectional structural subtyping for
14//!   other types.
15
16use crate::relations::compat::{AssignabilityOverrideProvider, ShapeExtractor, StringLikeVisitor};
17use crate::relations::subtype::TypeResolver;
18use crate::types::{LiteralValue, TypeData, TypeId};
19use crate::visitor::TypeVisitor;
20
21use super::compat::CompatChecker;
22
23// =============================================================================
24// Assignability Override Functions
25// =============================================================================
26
27impl<'a, R: TypeResolver> CompatChecker<'a, R> {
28    /// Check if `source` is assignable to `target` using TS compatibility rules,
29    /// with checker-provided overrides for enums, abstract constructors, and accessibility.
30    ///
31    /// This is the main entry point for assignability checking when checker context is available.
32    pub fn is_assignable_with_overrides<P: AssignabilityOverrideProvider + ?Sized>(
33        &mut self,
34        source: TypeId,
35        target: TypeId,
36        overrides: &P,
37    ) -> bool {
38        // Check override provider for enum assignability
39        if let Some(result) = overrides.enum_assignability_override(source, target) {
40            return result;
41        }
42
43        // Check override provider for abstract constructor assignability
44        if let Some(result) = overrides.abstract_constructor_assignability_override(source, target)
45        {
46            return result;
47        }
48
49        // Check override provider for constructor accessibility
50        if let Some(result) = overrides.constructor_accessibility_override(source, target) {
51            return result;
52        }
53
54        // Check private brand assignability (can be done with TypeDatabase alone)
55        if let Some(result) = self.private_brand_assignability_override(source, target) {
56            return result;
57        }
58
59        // Fall through to regular assignability check
60        self.is_assignable(source, target)
61    }
62
63    /// Private brand assignability override.
64    /// If both source and target types have private brands, they must match exactly.
65    /// This implements nominal typing for classes with private fields.
66    ///
67    /// Uses recursive structure to preserve Union/Intersection semantics:
68    /// - Union (A | B): OR logic - must satisfy at least one branch
69    /// - Intersection (A & B): AND logic - must satisfy all branches
70    pub fn private_brand_assignability_override(
71        &self,
72        source: TypeId,
73        target: TypeId,
74    ) -> Option<bool> {
75        use crate::types::Visibility;
76
77        // Fast path: identical types don't need nominal brand override logic.
78        // Let the regular assignability path decide.
79        if source == target {
80            return None;
81        }
82
83        // 1. Handle Target Union (OR logic)
84        // S -> (A | B) : Valid if S -> A OR S -> B
85        if let Some(TypeData::Union(members)) = self.interner.lookup(target) {
86            let members = self.interner.type_list(members);
87            // If source matches ANY target member, it's valid
88            for &member in members.iter() {
89                match self.private_brand_assignability_override(source, member) {
90                    Some(true) | None => return None, // Pass (or structural fallback)
91                    Some(false) => {}                 // Keep checking other members
92                }
93            }
94            return Some(false); // Failed against all members
95        }
96
97        // 2. Handle Source Union (AND logic)
98        // (A | B) -> T : Valid if A -> T AND B -> T
99        if let Some(TypeData::Union(members)) = self.interner.lookup(source) {
100            let members = self.interner.type_list(members);
101            for &member in members.iter() {
102                if let Some(false) = self.private_brand_assignability_override(member, target) {
103                    return Some(false); // Fail if any member fails
104                }
105            }
106            return None; // All passed or fell back
107        }
108
109        // 3. Handle Target Intersection (AND logic)
110        // S -> (A & B) : Valid if S -> A AND S -> B
111        if let Some(TypeData::Intersection(members)) = self.interner.lookup(target) {
112            let members = self.interner.type_list(members);
113            for &member in members.iter() {
114                if let Some(false) = self.private_brand_assignability_override(source, member) {
115                    return Some(false); // Fail if any member fails
116                }
117            }
118            return None; // All passed or fell back
119        }
120
121        // 4. Handle Source Intersection (OR logic)
122        // (A & B) -> T : Valid if A -> T OR B -> T
123        if let Some(TypeData::Intersection(members)) = self.interner.lookup(source) {
124            let members = self.interner.type_list(members);
125            for &member in members.iter() {
126                match self.private_brand_assignability_override(member, target) {
127                    Some(true) | None => return None, // Pass (or structural fallback)
128                    Some(false) => {}                 // Keep checking other members
129                }
130            }
131            return Some(false); // Failed against all members
132        }
133
134        // 5. Handle Lazy types (recursive resolution)
135        if let Some(TypeData::Lazy(def_id)) = self.interner.lookup(source)
136            && let Some(resolved) = self.subtype.resolver.resolve_lazy(def_id, self.interner)
137        {
138            // Guard against non-progressing lazy resolution (e.g. DefId -> same Lazy type),
139            // which would otherwise recurse forever.
140            if resolved == source {
141                return None;
142            }
143            return self.private_brand_assignability_override(resolved, target);
144        }
145
146        if let Some(TypeData::Lazy(def_id)) = self.interner.lookup(target)
147            && let Some(resolved) = self.subtype.resolver.resolve_lazy(def_id, self.interner)
148        {
149            // Same non-progress guard for target-side lazy resolution.
150            if resolved == target {
151                return None;
152            }
153            return self.private_brand_assignability_override(source, resolved);
154        }
155
156        // 6. Base case: Extract and compare object shapes
157        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
158
159        // Get source shape
160        let source_shape_id = extractor.extract(source)?;
161        let source_shape = self
162            .interner
163            .object_shape(crate::types::ObjectShapeId(source_shape_id));
164
165        // Get target shape
166        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
167        let target_shape_id = extractor.extract(target)?;
168        let target_shape = self
169            .interner
170            .object_shape(crate::types::ObjectShapeId(target_shape_id));
171
172        let mut has_private_brands = false;
173
174        // Check Target requirements (Nominality)
175        // If Target has a private/protected property, Source MUST match its origin exactly.
176        for target_prop in &target_shape.properties {
177            if target_prop.visibility == Visibility::Private
178                || target_prop.visibility == Visibility::Protected
179            {
180                has_private_brands = true;
181                let source_prop = source_shape
182                    .properties
183                    .binary_search_by_key(&target_prop.name, |p| p.name)
184                    .ok()
185                    .map(|idx| &source_shape.properties[idx]);
186
187                match source_prop {
188                    Some(sp) => {
189                        // CRITICAL: The parent_id must match exactly.
190                        if sp.parent_id != target_prop.parent_id {
191                            return Some(false);
192                        }
193                    }
194                    None => {
195                        return Some(false);
196                    }
197                }
198            }
199        }
200
201        // Check Source restrictions (Visibility leakage)
202        // If Source has a private/protected property, it cannot be assigned to a Target
203        // that expects it to be Public.
204        for source_prop in &source_shape.properties {
205            if source_prop.visibility == Visibility::Private
206                || source_prop.visibility == Visibility::Protected
207            {
208                has_private_brands = true;
209                if let Some(target_prop) = target_shape
210                    .properties
211                    .binary_search_by_key(&source_prop.name, |p| p.name)
212                    .ok()
213                    .map(|idx| &target_shape.properties[idx])
214                    && target_prop.visibility == Visibility::Public
215                {
216                    return Some(false);
217                }
218            }
219        }
220
221        has_private_brands.then_some(true)
222    }
223
224    /// Enum member assignability override.
225    /// Implements nominal typing for enum members: EnumA.X is NOT assignable to `EnumB` even if values match.
226    ///
227    /// TypeScript enum rules:
228    /// 1. Different enums with different `DefIds` are NOT assignable (nominal typing)
229    /// 2. Numeric enums are bidirectionally assignable to number (Rule #7 - Open Numeric Enums)
230    /// 3. String enums are strictly nominal (string literals NOT assignable to string enums)
231    /// 4. Same enum members with different values are NOT assignable (EnumA.X != EnumA.Y)
232    /// 5. Unions containing enums: Source union assigned to target enum checks all members
233    pub fn enum_assignability_override(&self, source: TypeId, target: TypeId) -> Option<bool> {
234        use crate::type_queries;
235        use crate::visitor;
236
237        // Special case: Source union -> Target enum
238        // When assigning a union to an enum, ALL enum members in the union must match the target enum.
239        // This handles cases like: (EnumA | EnumB) assigned to EnumC
240        // And allows: (Choice.Yes | Choice.No) assigned to Choice (subset of same enum)
241        if let Some((t_def, _)) = visitor::enum_components(self.interner, target)
242            && type_queries::is_union_type(self.interner, source)
243        {
244            let union_members = type_queries::get_union_members(self.interner, source)?;
245
246            let mut all_same_enum = true;
247            let mut has_non_enum = false;
248            for &member in &union_members {
249                if let Some((member_def, _)) = visitor::enum_components(self.interner, member) {
250                    // Check if this member belongs to the target enum.
251                    // Members have their own DefIds (different from parent enum's DefId),
252                    // so we must also check the parent relationship.
253                    let member_parent = self.subtype.resolver.get_enum_parent_def_id(member_def);
254                    if member_def != t_def && member_parent != Some(t_def) {
255                        // Found an enum member from a different enum than target
256                        return Some(false);
257                    }
258                } else {
259                    all_same_enum = false;
260                    has_non_enum = true;
261                }
262            }
263
264            // If ALL union members are enum members from the same enum as the target,
265            // the union is a subset of the enum and therefore assignable.
266            // This handles: `type YesNo = Choice.Yes | Choice.No` assignable to `Choice`.
267            if all_same_enum && !has_non_enum && !union_members.is_empty() {
268                return Some(true);
269            }
270            // Otherwise fall through to structural check for non-enum union members.
271        }
272
273        // String enums are assignable to string (like numeric enums are to number).
274        // Fall through to structural checking for this case.
275
276        // Fast path: Check if both are enum types with same DefId but different TypeIds
277        // This handles the test case where enum members aren't in the resolver
278        if let (Some((s_def, _)), Some((t_def, _))) = (
279            visitor::enum_components(self.interner, source),
280            visitor::enum_components(self.interner, target),
281        ) && s_def == t_def
282            && source != target
283        {
284            // Same enum DefId but different TypeIds
285            // Check if both are literal enum members (not union-based enums)
286            if self.is_literal_enum_member(source) && self.is_literal_enum_member(target) {
287                // Both are enum literals with same DefId but different values
288                // Nominal rule: E.A is NOT assignable to E.B
289                return Some(false);
290            }
291        }
292
293        let source_def = self.get_enum_def_id(source);
294        let target_def = self.get_enum_def_id(target);
295
296        match (source_def, target_def) {
297            // Case 1: Both are enums (or enum members or Union-based enums)
298            // Note: Same-DefId, different-TypeId case is now handled above before get_enum_def_id
299            (Some(s_def), Some(t_def)) => {
300                if s_def == t_def {
301                    // Same DefId: Same type (E.A -> E.A or E -> E)
302                    return Some(true);
303                }
304
305                // Gap A: Different DefIds, but might be member -> parent relationship
306                // Check if they share a parent enum (e.g., E.A -> E)
307                let s_parent = self.subtype.resolver.get_enum_parent_def_id(s_def);
308                let t_parent = self.subtype.resolver.get_enum_parent_def_id(t_def);
309
310                match (s_parent, t_parent) {
311                    (Some(sp), Some(tp)) if sp == tp => {
312                        // Same parent enum
313                        // If target is the Enum Type (e.g., 'E'), allow structural check
314                        if self.subtype.resolver.is_enum_type(target, self.interner) {
315                            return None;
316                        }
317                        // If target is a different specific member (e.g., 'E.B'), reject nominally
318                        // E.A -> E.B should fail even if they have the same value
319                        Some(false)
320                    }
321                    (Some(sp), None) => {
322                        // Source is a member, target doesn't have a parent (target is not a member)
323                        // Check if target is the parent enum type
324                        if t_def == sp {
325                            // Target is the parent enum of source member
326                            // Allow member to parent enum assignment (E.A -> E)
327                            return Some(true);
328                        }
329                        // Target is an enum type but not the parent
330                        Some(false)
331                    }
332                    _ => {
333                        // Different parents (or one/both are types, not members)
334                        // Nominal mismatch: EnumA.X is not assignable to EnumB
335                        Some(false)
336                    }
337                }
338            }
339
340            // Case 2: Target is an enum, source is a primitive
341            (None, Some(t_def)) => {
342                // Check if target is a numeric enum
343                if self.subtype.resolver.is_numeric_enum(t_def) {
344                    // Rule #7: Numeric enums allow number assignability
345                    // BUT we need to distinguish between:
346                    // - `let x: E = 1` (enum TYPE - allowed)
347                    // - `let x: E.A = 1` (enum MEMBER - rejected)
348
349                    // Check if source is number-like (number or number literal)
350                    let is_source_number = source == TypeId::NUMBER
351                        || matches!(
352                            self.interner.lookup(source),
353                            Some(TypeData::Literal(LiteralValue::Number(_)))
354                        );
355
356                    if is_source_number {
357                        // If target is the full Enum Type (e.g., `let x: E = 1`), allow it.
358                        if self.subtype.resolver.is_enum_type(target, self.interner) {
359                            // Allow bare `number` type but not arbitrary literals
360                            if source == TypeId::NUMBER {
361                                return Some(true);
362                            }
363                            // For number literals, fall through to structural check
364                            return None;
365                        }
366
367                        // If target is a specific member (e.g., `let x: E.A = 1`),
368                        // fall through to structural check.
369                        // - `1 -> E.A(0)` will fail structural check (Correct)
370                        // - `0 -> E.A(0)` will pass structural check (Correct)
371                        return None;
372                    }
373
374                    None
375                } else {
376                    // String enums do NOT allow raw string assignability
377                    // If source is string or string literal, reject
378                    if self.is_string_like(source) {
379                        return Some(false);
380                    }
381                    None
382                }
383            }
384
385            // Case 3: Source is an enum, target is a primitive
386            // String enums (both types and members) are assignable to string via structural checking
387            (Some(s_def), None) => {
388                // Check if source is a string enum
389                if !self.subtype.resolver.is_numeric_enum(s_def) {
390                    // Source is a string enum
391                    if target == TypeId::STRING {
392                        // Both enum types (Union of members) and enum members (string literals)
393                        // are assignable to string. Fall through to structural checking.
394                        return None;
395                    }
396                }
397                // Numeric enums and non-string targets: fall through to structural check
398                None
399            }
400
401            // Case 4: Neither is an enum
402            (None, None) => None,
403        }
404    }
405
406    /// Check if a type is string-like (string, string literal, or template literal).
407    /// Used to reject primitive-to-string-enum assignments.
408    fn is_string_like(&self, type_id: TypeId) -> bool {
409        if type_id == TypeId::STRING {
410            return true;
411        }
412        // Use visitor to check for string literals, template literals, etc.
413        let mut visitor = StringLikeVisitor { db: self.interner };
414        visitor.visit_type(self.interner, type_id)
415    }
416
417    /// Get the `DefId` of an enum type, handling both direct Enum members and Union-based Enums.
418    /// Check whether `type_id` is an enum whose underlying member is a string or number literal.
419    fn is_literal_enum_member(&self, type_id: TypeId) -> bool {
420        matches!(
421            self.interner.lookup(type_id),
422            Some(TypeData::Enum(_, member_type))
423                if matches!(
424                    self.interner.lookup(member_type),
425                    Some(TypeData::Literal(LiteralValue::Number(_) | LiteralValue::String(_)))
426                )
427        )
428    }
429
430    /// Returns `Some(def_id)` if the type is an Enum or a Union of Enum members from the same enum.
431    /// Returns None if the type is not an enum or contains mixed enums.
432    fn get_enum_def_id(&self, type_id: TypeId) -> Option<crate::def::DefId> {
433        use crate::{type_queries, visitor};
434
435        // Resolve Lazy types first (handles imported/forward-declared enums)
436        let resolved =
437            if let Some(lazy_def_id) = type_queries::get_lazy_def_id(self.interner, type_id) {
438                // Try to resolve the Lazy type
439                if let Some(resolved_type) = self
440                    .subtype
441                    .resolver
442                    .resolve_lazy(lazy_def_id, self.interner)
443                {
444                    // Guard against self-referential lazy types
445                    if resolved_type == type_id {
446                        return None;
447                    }
448                    // Recursively check the resolved type
449                    return self.get_enum_def_id(resolved_type);
450                }
451                // Lazy type couldn't be resolved yet, return None
452                return None;
453            } else {
454                type_id
455            };
456
457        // 1. Check for Intrinsic Primitives first (using visitor, not TypeId constants)
458        // This filters out intrinsic types like string, number, boolean which are stored
459        // as TypeData::Enum for definition store purposes but are NOT user enums
460        if visitor::intrinsic_kind(self.interner, resolved).is_some() {
461            return None;
462        }
463
464        // 2. Check direct Enum member
465        if let Some((def_id, _inner)) = visitor::enum_components(self.interner, resolved) {
466            // Use the new is_user_enum_def method to check if this is a user-defined enum
467            // This properly filters out intrinsic types from lib.d.ts
468            if self.subtype.resolver.is_user_enum_def(def_id) {
469                return Some(def_id);
470            }
471            // Not a user-defined enum (intrinsic type or type alias)
472            return None;
473        }
474
475        // 3. Check Union of Enum members (handles Enum types represented as Unions)
476        if let Some(members) = visitor::union_list_id(self.interner, resolved) {
477            let members = self.interner.type_list(members);
478            if members.is_empty() {
479                return None;
480            }
481
482            let first_def = self.get_enum_def_id(members[0])?;
483            for &member in members.iter().skip(1) {
484                if self.get_enum_def_id(member) != Some(first_def) {
485                    return None; // Mixed union or non-enum members
486                }
487            }
488            return Some(first_def);
489        }
490
491        None
492    }
493
494    /// Checks if two types are compatible for variable redeclaration (TS2403).
495    ///
496    /// This applies TypeScript's nominal identity rules for enums and
497    /// respects 'any' propagation. Used for checking if multiple variable
498    /// declarations have compatible types.
499    ///
500    /// # Examples
501    /// - `var x: number; var x: number` → true
502    /// - `var x: E.A; var x: E.A` → true
503    /// - `var x: E.A; var x: E.B` → false
504    /// - `var x: E; var x: F` → false (different enums)
505    /// - `var x: E; var x: number` → false
506    pub fn are_types_identical_for_redeclaration(&mut self, a: TypeId, b: TypeId) -> bool {
507        // 1. Fast path: physical identity
508        if a == b {
509            return true;
510        }
511
512        // 2. Error propagation — suppress cascading errors from ERROR types.
513        if a == TypeId::ERROR || b == TypeId::ERROR {
514            return true;
515        }
516
517        // For redeclaration, `any` is only identical to `any`.
518        // `a == b` already caught the `any == any` case above.
519        if a == TypeId::ANY || b == TypeId::ANY {
520            return false;
521        }
522
523        // 4. Enum Nominality Check
524        // If one is an enum and the other isn't, or they are different enums,
525        // they are not identical for redeclaration, even if structurally compatible.
526        if let Some(res) = self.enum_redeclaration_check(a, b) {
527            return res;
528        }
529
530        // 5. Normalize Application/Mapped/Lazy types before structural comparison.
531        // Required<{a?: string}> must evaluate to {a: string} before bidirectional
532        // subtype checking, just as is_assignable_impl() does via normalize_assignability_operands.
533        let (a_norm, b_norm) = self.normalize_assignability_operands(a, b);
534        tracing::trace!(
535            a = a.0,
536            b = b.0,
537            a_norm = a_norm.0,
538            b_norm = b_norm.0,
539            a_changed = a != a_norm,
540            b_changed = b != b_norm,
541            "are_types_identical_for_redeclaration: normalized"
542        );
543        let a = a_norm;
544        let b = b_norm;
545
546        // 5. Structural Identity
547        // Delegate to the Judge to check bidirectional subtyping
548        let fwd = self.subtype.is_subtype_of(a, b);
549        let bwd = self.subtype.is_subtype_of(b, a);
550        tracing::trace!(
551            a = a.0,
552            b = b.0,
553            fwd,
554            bwd,
555            "are_types_identical_for_redeclaration: result"
556        );
557        fwd && bwd
558    }
559
560    /// Check if two types involving enums are compatible for redeclaration.
561    ///
562    /// Returns Some(bool) if either type is an enum:
563    /// - Some(false) if different enums or enum vs primitive
564    /// - None if neither is an enum (delegate to structural check)
565    fn enum_redeclaration_check(&self, a: TypeId, b: TypeId) -> Option<bool> {
566        let a_def = self.get_enum_def_id(a);
567        let b_def = self.get_enum_def_id(b);
568
569        match (a_def, b_def) {
570            (Some(def_a), Some(def_b)) => {
571                // Both are enums: must be the same enum definition
572                if def_a != def_b {
573                    Some(false)
574                } else {
575                    // Same enum DefId: compatible for redeclaration
576                    // This allows: var x: MyEnum; var x = MyEnum.Member;
577                    // where MyEnum.Member (enum member) is compatible with MyEnum (enum type)
578                    Some(true)
579                }
580            }
581            (Some(_), None) | (None, Some(_)) => {
582                // One is an enum, the other is a primitive (e.g., number)
583                // In TS, Enum E and 'number' are NOT identical for redeclaration
584                Some(false)
585            }
586            (None, None) => None,
587        }
588    }
589}