Skip to main content

tsz_solver/
compat.rs

1//! TypeScript compatibility layer for assignability rules.
2
3use crate::db::QueryDatabase;
4use crate::diagnostics::SubtypeFailureReason;
5use crate::subtype::{NoopResolver, SubtypeChecker, TypeResolver};
6use crate::types::{IntrinsicKind, LiteralValue, PropertyInfo, TypeData, TypeId};
7use crate::visitor::{TypeVisitor, intrinsic_kind, is_empty_object_type_db, lazy_def_id};
8use crate::{AnyPropagationRules, AssignabilityChecker, TypeDatabase};
9use rustc_hash::FxHashMap;
10use tsz_common::interner::Atom;
11
12// =============================================================================
13// Visitor Pattern Implementations
14// =============================================================================
15
16/// Visitor to extract object shape ID from types.
17struct ShapeExtractor<'a, R: TypeResolver> {
18    db: &'a dyn TypeDatabase,
19    resolver: &'a R,
20    guard: crate::recursion::RecursionGuard<TypeId>,
21}
22
23impl<'a, R: TypeResolver> ShapeExtractor<'a, R> {
24    fn new(db: &'a dyn TypeDatabase, resolver: &'a R) -> Self {
25        Self {
26            db,
27            resolver,
28            guard: crate::recursion::RecursionGuard::with_profile(
29                crate::recursion::RecursionProfile::ShapeExtraction,
30            ),
31        }
32    }
33
34    /// Extract shape from a type, returning None if not an object type.
35    fn extract(&mut self, type_id: TypeId) -> Option<u32> {
36        match self.guard.enter(type_id) {
37            crate::recursion::RecursionResult::Entered => {}
38            _ => return None, // Cycle or limits exceeded
39        }
40        let result = self.visit_type(self.db, type_id);
41        self.guard.leave(type_id);
42        result
43    }
44}
45
46/// Visitor to check if a type is string-like (string, string literal, or template literal).
47struct StringLikeVisitor<'a> {
48    db: &'a dyn TypeDatabase,
49}
50
51impl<'a> TypeVisitor for StringLikeVisitor<'a> {
52    type Output = bool;
53
54    fn visit_intrinsic(&mut self, kind: IntrinsicKind) -> Self::Output {
55        kind == IntrinsicKind::String
56    }
57
58    fn visit_literal(&mut self, value: &LiteralValue) -> Self::Output {
59        matches!(value, LiteralValue::String(_))
60    }
61
62    fn visit_template_literal(&mut self, _template_id: u32) -> Self::Output {
63        true
64    }
65
66    fn visit_type_parameter(&mut self, info: &crate::types::TypeParamInfo) -> Self::Output {
67        info.constraint.is_some_and(|c| self.visit_type(self.db, c))
68    }
69
70    fn visit_ref(&mut self, symbol_ref: u32) -> Self::Output {
71        let _symbol_ref = crate::types::SymbolRef(symbol_ref);
72        // Resolve the ref and check the resolved type
73        // This is a simplified check - in practice we'd need the resolver
74        false
75    }
76
77    fn visit_lazy(&mut self, _def_id: u32) -> Self::Output {
78        // We can't resolve Lazy without a resolver, so conservatively return false
79        false
80    }
81
82    fn default_output() -> Self::Output {
83        false
84    }
85}
86
87impl<'a, R: TypeResolver> TypeVisitor for ShapeExtractor<'a, R> {
88    type Output = Option<u32>;
89
90    fn visit_intrinsic(&mut self, _kind: crate::types::IntrinsicKind) -> Self::Output {
91        None
92    }
93
94    fn visit_literal(&mut self, _value: &crate::LiteralValue) -> Self::Output {
95        None
96    }
97
98    fn visit_object(&mut self, shape_id: u32) -> Self::Output {
99        Some(shape_id)
100    }
101
102    fn visit_object_with_index(&mut self, shape_id: u32) -> Self::Output {
103        Some(shape_id)
104    }
105
106    fn visit_lazy(&mut self, def_id: u32) -> Self::Output {
107        let def_id = crate::def::DefId(def_id);
108        if let Some(resolved) = self.resolver.resolve_lazy(def_id, self.db) {
109            return self.extract(resolved);
110        }
111        None
112    }
113
114    fn visit_ref(&mut self, symbol_ref: u32) -> Self::Output {
115        let symbol_ref = crate::types::SymbolRef(symbol_ref);
116        // Prefer DefId resolution if available
117        if let Some(def_id) = self.resolver.symbol_to_def_id(symbol_ref) {
118            return self.visit_lazy(def_id.0);
119        }
120        if let Some(resolved) = self.resolver.resolve_symbol_ref(symbol_ref, self.db) {
121            return self.extract(resolved);
122        }
123        None
124    }
125
126    // TSZ-4: Handle Intersection types for nominal checking
127    // For private brands, we need to find object shapes within the intersection
128    fn visit_intersection(&mut self, list_id: u32) -> Self::Output {
129        let member_list = self.db.type_list(crate::types::TypeListId(list_id));
130        // For nominal checking, iterate and return the first valid object shape found
131        // This ensures we check the private/protected members of constituent types
132        for member in member_list.iter() {
133            if let Some(shape) = self.visit_type(self.db, *member) {
134                return Some(shape);
135            }
136        }
137        None
138    }
139
140    fn default_output() -> Self::Output {
141        None
142    }
143}
144
145/// Trait for providing checker-specific assignability overrides.
146///
147/// This allows the solver's `CompatChecker` to call back into the checker
148/// for special cases that require binder/symbol information (enums,
149/// abstract constructors, constructor accessibility).
150pub trait AssignabilityOverrideProvider {
151    /// Override for enum assignability rules.
152    /// Returns Some(true/false) if the override applies, None to fall through to structural checking.
153    fn enum_assignability_override(&self, source: TypeId, target: TypeId) -> Option<bool>;
154
155    /// Override for abstract constructor assignability rules.
156    /// Returns Some(false) if abstract class cannot be assigned to concrete constructor, None otherwise.
157    fn abstract_constructor_assignability_override(
158        &self,
159        source: TypeId,
160        target: TypeId,
161    ) -> Option<bool>;
162
163    /// Override for constructor accessibility rules (private/protected).
164    /// Returns Some(false) if accessibility mismatch prevents assignment, None otherwise.
165    fn constructor_accessibility_override(&self, source: TypeId, target: TypeId) -> Option<bool>;
166}
167
168/// A no-op implementation of `AssignabilityOverrideProvider` for when no checker context is available.
169pub struct NoopOverrideProvider;
170
171impl AssignabilityOverrideProvider for NoopOverrideProvider {
172    fn enum_assignability_override(&self, _source: TypeId, _target: TypeId) -> Option<bool> {
173        None
174    }
175
176    fn abstract_constructor_assignability_override(
177        &self,
178        _source: TypeId,
179        _target: TypeId,
180    ) -> Option<bool> {
181        None
182    }
183
184    fn constructor_accessibility_override(&self, _source: TypeId, _target: TypeId) -> Option<bool> {
185        None
186    }
187}
188
189/// Compatibility checker that applies TypeScript's unsound rules
190/// before delegating to the structural subtype engine.
191///
192/// This layer integrates with the "Lawyer" layer to apply nuanced rules
193/// for `any` propagation.
194pub struct CompatChecker<'a, R: TypeResolver = NoopResolver> {
195    interner: &'a dyn TypeDatabase,
196    /// Optional query database for Salsa-backed memoization.
197    query_db: Option<&'a dyn QueryDatabase>,
198    subtype: SubtypeChecker<'a, R>,
199    /// The "Lawyer" layer - handles nuanced rules for `any` propagation.
200    lawyer: AnyPropagationRules,
201    strict_function_types: bool,
202    strict_null_checks: bool,
203    no_unchecked_indexed_access: bool,
204    exact_optional_property_types: bool,
205    /// When true, enables additional strict subtype checking rules for lib.d.ts
206    strict_subtype_checking: bool,
207    cache: FxHashMap<(TypeId, TypeId), bool>,
208}
209
210impl<'a> CompatChecker<'a, NoopResolver> {
211    /// Create a new compatibility checker without a resolver.
212    /// Note: Callers should configure `strict_function_types` explicitly via `set_strict_function_types()`
213    pub fn new(interner: &'a dyn TypeDatabase) -> Self {
214        CompatChecker {
215            interner,
216            query_db: None,
217            subtype: SubtypeChecker::new(interner),
218            lawyer: AnyPropagationRules::new(),
219            // Default to false (legacy TypeScript behavior) for compatibility
220            // Callers should set this explicitly based on compiler options
221            strict_function_types: false,
222            strict_null_checks: true,
223            no_unchecked_indexed_access: false,
224            exact_optional_property_types: false,
225            strict_subtype_checking: false,
226            cache: FxHashMap::default(),
227        }
228    }
229}
230
231impl<'a, R: TypeResolver> CompatChecker<'a, R> {
232    fn normalize_assignability_operand(&mut self, mut type_id: TypeId) -> TypeId {
233        // Keep normalization bounded to avoid infinite resolver/evaluator cycles.
234        for _ in 0..8 {
235            let next = match self.interner.lookup(type_id) {
236                Some(TypeData::Lazy(def_id)) => self
237                    .subtype
238                    .resolver
239                    .resolve_lazy(def_id, self.interner)
240                    .unwrap_or(type_id),
241                Some(TypeData::Mapped(_) | TypeData::Application(_)) => {
242                    self.subtype.evaluate_type(type_id)
243                }
244                _ => type_id,
245            };
246
247            if next == type_id {
248                break;
249            }
250            type_id = next;
251        }
252        type_id
253    }
254
255    fn normalize_assignability_operands(
256        &mut self,
257        source: TypeId,
258        target: TypeId,
259    ) -> (TypeId, TypeId) {
260        (
261            self.normalize_assignability_operand(source),
262            self.normalize_assignability_operand(target),
263        )
264    }
265
266    fn is_function_target_member(&self, member: TypeId) -> bool {
267        let is_function_object_shape = match self.interner.lookup(member) {
268            Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
269                let shape = self.interner.object_shape(shape_id);
270                let apply = self.interner.intern_string("apply");
271                let call = self.interner.intern_string("call");
272                let has_apply = shape.properties.iter().any(|prop| prop.name == apply);
273                let has_call = shape.properties.iter().any(|prop| prop.name == call);
274                has_apply && has_call
275            }
276            _ => false,
277        };
278
279        intrinsic_kind(self.interner, member) == Some(IntrinsicKind::Function)
280            || is_function_object_shape
281            || self
282                .subtype
283                .resolver
284                .get_boxed_type(IntrinsicKind::Function)
285                .is_some_and(|boxed| boxed == member)
286            || lazy_def_id(self.interner, member).is_some_and(|def_id| {
287                self.subtype
288                    .resolver
289                    .is_boxed_def_id(def_id, IntrinsicKind::Function)
290            })
291    }
292
293    /// Create a new compatibility checker with a resolver.
294    /// Note: Callers should configure `strict_function_types` explicitly via `set_strict_function_types()`
295    pub fn with_resolver(interner: &'a dyn TypeDatabase, resolver: &'a R) -> Self {
296        CompatChecker {
297            interner,
298            query_db: None,
299            subtype: SubtypeChecker::with_resolver(interner, resolver),
300            lawyer: AnyPropagationRules::new(),
301            // Default to false (legacy TypeScript behavior) for compatibility
302            // Callers should set this explicitly based on compiler options
303            strict_function_types: false,
304            strict_null_checks: true,
305            no_unchecked_indexed_access: false,
306            exact_optional_property_types: false,
307            strict_subtype_checking: false,
308            cache: FxHashMap::default(),
309        }
310    }
311
312    /// Set the query database for Salsa-backed memoization.
313    /// Propagates to the internal `SubtypeChecker`.
314    pub fn set_query_db(&mut self, db: &'a dyn QueryDatabase) {
315        self.query_db = Some(db);
316        self.subtype.query_db = Some(db);
317    }
318
319    /// Set the inheritance graph for nominal class subtype checking.
320    /// Propagates to the internal `SubtypeChecker`.
321    pub const fn set_inheritance_graph(
322        &mut self,
323        graph: Option<&'a crate::inheritance::InheritanceGraph>,
324    ) {
325        self.subtype.inheritance_graph = graph;
326    }
327
328    /// Configure strict function parameter checking.
329    /// See <https://github.com/microsoft/TypeScript/issues/18654>.
330    pub fn set_strict_function_types(&mut self, strict: bool) {
331        if self.strict_function_types != strict {
332            self.strict_function_types = strict;
333            self.cache.clear();
334        }
335    }
336
337    /// Configure strict null checks (legacy null/undefined assignability).
338    pub fn set_strict_null_checks(&mut self, strict: bool) {
339        if self.strict_null_checks != strict {
340            self.strict_null_checks = strict;
341            self.cache.clear();
342        }
343    }
344
345    /// Configure unchecked indexed access (include `undefined` in `T[K]`).
346    pub fn set_no_unchecked_indexed_access(&mut self, enabled: bool) {
347        if self.no_unchecked_indexed_access != enabled {
348            self.no_unchecked_indexed_access = enabled;
349            self.cache.clear();
350        }
351    }
352
353    /// Configure exact optional property types.
354    /// See <https://github.com/microsoft/TypeScript/issues/13195>.
355    pub fn set_exact_optional_property_types(&mut self, exact: bool) {
356        if self.exact_optional_property_types != exact {
357            self.exact_optional_property_types = exact;
358            self.cache.clear();
359        }
360    }
361
362    /// Configure strict mode for `any` propagation.
363    /// Configure strict subtype checking mode for lib.d.ts type checking.
364    ///
365    /// When enabled, applies additional strictness rules that reject borderline
366    /// cases allowed by TypeScript's legacy behavior. This includes disabling
367    /// method bivariance for soundness.
368    pub fn set_strict_subtype_checking(&mut self, strict: bool) {
369        if self.strict_subtype_checking != strict {
370            self.strict_subtype_checking = strict;
371            self.cache.clear();
372        }
373    }
374
375    /// Apply compiler options from a bitmask flags value.
376    ///
377    /// The flags correspond to `RelationCacheKey` bits:
378    /// - bit 0: `strict_null_checks`
379    /// - bit 1: `strict_function_types`
380    /// - bit 2: `exact_optional_property_types`
381    /// - bit 3: `no_unchecked_indexed_access`
382    /// - bit 4: `disable_method_bivariance` (`strict_subtype_checking`)
383    /// - bit 5: `allow_void_return`
384    /// - bit 6: `allow_bivariant_rest`
385    /// - bit 7: `allow_bivariant_param_count`
386    ///
387    /// This is used by `QueryCache::is_assignable_to_with_flags` to ensure
388    /// cached results respect the compiler configuration.
389    pub fn apply_flags(&mut self, flags: u16) {
390        // Apply flags to CompatChecker's own fields
391        let strict_null_checks = (flags & (1 << 0)) != 0;
392        let strict_function_types = (flags & (1 << 1)) != 0;
393        let exact_optional_property_types = (flags & (1 << 2)) != 0;
394        let no_unchecked_indexed_access = (flags & (1 << 3)) != 0;
395        let disable_method_bivariance = (flags & (1 << 4)) != 0;
396
397        self.set_strict_null_checks(strict_null_checks);
398        self.set_strict_function_types(strict_function_types);
399        self.set_exact_optional_property_types(exact_optional_property_types);
400        self.set_no_unchecked_indexed_access(no_unchecked_indexed_access);
401        self.set_strict_subtype_checking(disable_method_bivariance);
402
403        // Also apply flags to the internal SubtypeChecker
404        // We do this directly since apply_flags() uses a builder pattern
405        self.subtype.strict_null_checks = strict_null_checks;
406        self.subtype.strict_function_types = strict_function_types;
407        self.subtype.exact_optional_property_types = exact_optional_property_types;
408        self.subtype.no_unchecked_indexed_access = no_unchecked_indexed_access;
409        self.subtype.disable_method_bivariance = disable_method_bivariance;
410        self.subtype.allow_void_return = (flags & (1 << 5)) != 0;
411        self.subtype.allow_bivariant_rest = (flags & (1 << 6)) != 0;
412        self.subtype.allow_bivariant_param_count = (flags & (1 << 7)) != 0;
413    }
414
415    ///
416    /// When strict mode is enabled, `any` does NOT silence structural mismatches.
417    /// This means the type checker will still report errors even when `any` is involved,
418    /// if there's a real structural mismatch.
419    pub fn set_strict_any_propagation(&mut self, strict: bool) {
420        self.lawyer.set_allow_any_suppression(!strict);
421        self.cache.clear();
422    }
423
424    /// Get a reference to the lawyer layer for `any` propagation rules.
425    pub const fn lawyer(&self) -> &AnyPropagationRules {
426        &self.lawyer
427    }
428
429    /// Get a mutable reference to the lawyer layer for `any` propagation rules.
430    pub fn lawyer_mut(&mut self) -> &mut AnyPropagationRules {
431        self.cache.clear();
432        &mut self.lawyer
433    }
434
435    /// Apply configuration from `JudgeConfig`.
436    ///
437    /// This is used to configure the `CompatChecker` with settings from
438    /// the `CompilerOptions` (passed through `JudgeConfig`).
439    pub fn apply_config(&mut self, config: &crate::judge::JudgeConfig) {
440        self.strict_function_types = config.strict_function_types;
441        self.strict_null_checks = config.strict_null_checks;
442        self.exact_optional_property_types = config.exact_optional_property_types;
443        self.no_unchecked_indexed_access = config.no_unchecked_indexed_access;
444
445        // North Star: any should NOT silence structural mismatches in strict mode
446        self.lawyer.allow_any_suppression = !config.strict_function_types && !config.sound_mode;
447
448        // Clear cache as configuration changed
449        self.cache.clear();
450    }
451
452    /// Check if `source` is assignable to `target` using TS compatibility rules.
453    pub fn is_assignable(&mut self, source: TypeId, target: TypeId) -> bool {
454        // Without strictNullChecks, null and undefined are assignable to and from any type.
455        // This check is at the top-level only (not in subtype member iteration) to avoid
456        // incorrectly accepting types within union member comparisons.
457        if !self.strict_null_checks && target.is_nullish() {
458            return true;
459        }
460
461        let key = (source, target);
462        if let Some(&cached) = self.cache.get(&key) {
463            return cached;
464        }
465
466        let result = self.is_assignable_impl(source, target, self.strict_function_types);
467
468        self.cache.insert(key, result);
469        result
470    }
471
472    /// Check for excess properties in object literal assignment (TS2353).
473    ///
474    /// This implements the "Lawyer" layer rule where fresh object literals
475    /// cannot have properties that don't exist in the target type, unless the
476    /// target has an index signature.
477    ///
478    /// # Arguments
479    /// * `source` - The source type (should be a fresh object literal)
480    /// * `target` - The target type
481    ///
482    /// # Returns
483    /// `true` if no excess properties found, `false` if TS2353 should be reported
484    fn check_excess_properties(&mut self, source: TypeId, target: TypeId) -> bool {
485        use crate::freshness::is_fresh_object_type;
486        use crate::visitor::{ObjectTypeKind, classify_object_type};
487
488        // Only check fresh object literals
489        if !is_fresh_object_type(self.interner, source) {
490            return true;
491        }
492
493        // Get source shape
494        let source_shape_id = match classify_object_type(self.interner, source) {
495            ObjectTypeKind::Object(shape_id) | ObjectTypeKind::ObjectWithIndex(shape_id) => {
496                shape_id
497            }
498            ObjectTypeKind::NotObject => return true,
499        };
500
501        let source_shape = self.interner.object_shape(source_shape_id);
502
503        let (has_string_index, has_number_index) = self.check_index_signatures(target);
504
505        // If target has string index signature, skip excess property check entirely
506        if has_string_index {
507            return true;
508        }
509
510        // Collect all target properties (including base types if intersection)
511        let target_properties = self.collect_target_properties(target);
512
513        // TypeScript forgives excess properties when the target type is completely empty
514        // (like `{}`, an empty interface, or an empty class) because it accepts any non-primitive.
515        if target_properties.is_empty() && !has_number_index {
516            return true;
517        }
518
519        // Check each source property
520        for prop_info in &source_shape.properties {
521            if !target_properties.contains(&prop_info.name) {
522                // If target has a numeric index signature, numeric-named properties are allowed
523                if has_number_index {
524                    let name_str = self.interner.resolve_atom(prop_info.name);
525                    if name_str.parse::<f64>().is_ok() {
526                        continue;
527                    }
528                }
529                // Excess property found!
530                return false;
531            }
532        }
533
534        true
535    }
536
537    /// Find the first excess property in object literal assignment.
538    ///
539    /// Returns `Some(property_name)` if an excess property is found, `None` otherwise.
540    /// This is used by `explain_failure` to generate TS2353 diagnostics.
541    fn find_excess_property(&mut self, source: TypeId, target: TypeId) -> Option<Atom> {
542        use crate::freshness::is_fresh_object_type;
543        use crate::visitor::{ObjectTypeKind, classify_object_type};
544
545        // Only check fresh object literals
546        if !is_fresh_object_type(self.interner, source) {
547            return None;
548        }
549
550        // Get source shape
551        let source_shape_id = match classify_object_type(self.interner, source) {
552            ObjectTypeKind::Object(shape_id) | ObjectTypeKind::ObjectWithIndex(shape_id) => {
553                shape_id
554            }
555            ObjectTypeKind::NotObject => return None,
556        };
557
558        let source_shape = self.interner.object_shape(source_shape_id);
559
560        // Get target shape - resolve Lazy, Mapped, and Application types
561        let target_key = self.interner.lookup(target);
562        let resolved_target = match target_key {
563            Some(TypeData::Lazy(def_id)) => {
564                // Try to resolve the Lazy type
565                self.subtype.resolver.resolve_lazy(def_id, self.interner)?
566            }
567            Some(TypeData::Mapped(_) | TypeData::Application(_)) => {
568                // Evaluate mapped and application types
569                self.subtype.evaluate_type(target)
570            }
571            _ => target,
572        };
573
574        let (has_string_index, has_number_index) = self.check_index_signatures(resolved_target);
575
576        // If target has string index signature, skip excess property check entirely
577        if has_string_index {
578            return None;
579        }
580
581        // Collect all target properties (including base types if intersection)
582        let target_properties = self.collect_target_properties(resolved_target);
583
584        // TypeScript forgives excess properties when the target type is completely empty
585        if target_properties.is_empty() && !has_number_index {
586            return None;
587        }
588
589        // Check each source property
590        for prop_info in &source_shape.properties {
591            if !target_properties.contains(&prop_info.name) {
592                // If target has a numeric index signature, numeric-named properties are allowed
593                if has_number_index {
594                    let name_str = self.interner.resolve_atom(prop_info.name);
595                    if name_str.parse::<f64>().is_ok() {
596                        continue;
597                    }
598                }
599                // Excess property found!
600                return Some(prop_info.name);
601            }
602        }
603
604        None
605    }
606
607    /// Collect all property names from a type into a set (handles intersections and unions).
608    ///
609    /// For intersections: property exists if it's in ANY member
610    /// For unions: property exists if it's in ALL members
611    /// Check if a type or any of its composite members has a string or numeric index signature.
612    /// Returns `(has_string_index, has_number_index)`.
613    fn check_index_signatures(&mut self, type_id: TypeId) -> (bool, bool) {
614        if type_id == TypeId::ANY || type_id == TypeId::UNKNOWN || type_id == TypeId::ERROR {
615            return (true, true);
616        }
617
618        let type_id = match self.interner.lookup(type_id) {
619            Some(TypeData::Lazy(def_id)) => self
620                .subtype
621                .resolver
622                .resolve_lazy(def_id, self.interner)
623                .unwrap_or(type_id),
624            Some(TypeData::Mapped(_) | TypeData::Application(_)) => {
625                self.subtype.evaluate_type(type_id)
626            }
627            _ => type_id,
628        };
629
630        if type_id == TypeId::ANY || type_id == TypeId::UNKNOWN || type_id == TypeId::ERROR {
631            return (true, true);
632        }
633
634        match self.interner.lookup(type_id) {
635            Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
636                let shape = self.interner.object_shape(shape_id);
637                (shape.string_index.is_some(), shape.number_index.is_some())
638            }
639            Some(TypeData::Intersection(members_id)) | Some(TypeData::Union(members_id)) => {
640                let members = self.interner.type_list(members_id);
641                let mut has_str = false;
642                let mut has_num = false;
643                for &member in members.iter() {
644                    let (s, n) = self.check_index_signatures(member);
645                    has_str |= s;
646                    has_num |= n;
647                }
648                (has_str, has_num)
649            }
650            _ => (false, false),
651        }
652    }
653
654    fn collect_target_properties(&mut self, type_id: TypeId) -> rustc_hash::FxHashSet<Atom> {
655        // Handle Mapped and Application types by evaluating them to concrete types
656        // We resolve before matching so the existing logic handles the result.
657        let type_id = match self.interner.lookup(type_id) {
658            Some(TypeData::Mapped(_) | TypeData::Application(_)) => {
659                self.subtype.evaluate_type(type_id)
660            }
661            _ => type_id,
662        };
663
664        let mut properties = rustc_hash::FxHashSet::default();
665
666        match self.interner.lookup(type_id) {
667            Some(TypeData::Intersection(members_id)) => {
668                let members = self.interner.type_list(members_id);
669                // Property exists if it's in ANY member of intersection
670                for &member in members.iter() {
671                    let member_props = self.collect_target_properties(member);
672                    properties.extend(member_props);
673                }
674            }
675            Some(TypeData::Union(members_id)) => {
676                let members = self.interner.type_list(members_id);
677                if members.is_empty() {
678                    return properties;
679                }
680                // For unions, property exists if it's in ALL members
681                // Start with first member's properties
682                let mut all_props = self.collect_target_properties(members[0]);
683                // Intersect with remaining members
684                for &member in members.iter().skip(1) {
685                    let member_props = self.collect_target_properties(member);
686                    all_props = all_props.intersection(&member_props).cloned().collect();
687                }
688                properties = all_props;
689            }
690            Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
691                let shape = self.interner.object_shape(shape_id);
692                for prop_info in &shape.properties {
693                    properties.insert(prop_info.name);
694                }
695            }
696            _ => {}
697        }
698
699        properties
700    }
701
702    /// Internal implementation of assignability check.
703    /// Extracted to share logic between `is_assignable` and `is_assignable_strict`.
704    fn is_assignable_impl(
705        &mut self,
706        source: TypeId,
707        target: TypeId,
708        strict_function_types: bool,
709    ) -> bool {
710        let (source, target) = self.normalize_assignability_operands(source, target);
711
712        // Fast path checks
713        if let Some(result) = self.check_assignable_fast_path(source, target) {
714            return result;
715        }
716
717        // Enum nominal typing check (Lawyer layer implementation)
718        // This provides enum member distinction even without checker context
719        if let Some(result) = self.enum_assignability_override(source, target) {
720            return result;
721        }
722
723        // Weak type checks
724        if self.violates_weak_union(source, target) {
725            return false;
726        }
727        if self.violates_weak_type(source, target) {
728            return false;
729        }
730
731        // Excess property checking (TS2353) - Lawyer layer
732        if !self.check_excess_properties(source, target) {
733            return false;
734        }
735
736        // Empty object target
737        if self.is_empty_object_target(target) {
738            return self.is_assignable_to_empty_object(source);
739        }
740
741        // Check mapped-to-mapped structural comparison before full subtype check.
742        // When both source and target are deferred mapped types over the same constraint
743        // (e.g., Readonly<T> vs Partial<T>), compare template types directly.
744        if let (Some(TypeData::Mapped(s_mapped_id)), Some(TypeData::Mapped(t_mapped_id))) =
745            (self.interner.lookup(source), self.interner.lookup(target))
746        {
747            let result = self.check_mapped_to_mapped_assignability(s_mapped_id, t_mapped_id);
748            if let Some(assignable) = result {
749                return assignable;
750            }
751        }
752
753        // Default to structural subtype checking
754        self.configure_subtype(strict_function_types);
755        self.subtype.is_subtype_of(source, target)
756    }
757
758    /// Check if two mapped types are assignable via structural template comparison.
759    ///
760    /// When both source and target are mapped types with the same constraint
761    /// (e.g., both iterate over `keyof T`), compare their templates directly.
762    /// This handles cases like `Readonly<T>` assignable to `Partial<T>` where
763    /// the mapped types can't be concretely expanded because T is generic.
764    ///
765    /// Returns `Some(true/false)` if determination was made, `None` to fall through.
766    fn check_mapped_to_mapped_assignability(
767        &mut self,
768        s_mapped_id: crate::types::MappedTypeId,
769        t_mapped_id: crate::types::MappedTypeId,
770    ) -> Option<bool> {
771        use crate::types::MappedModifier;
772        use crate::visitor::mapped_type_id;
773
774        let s_mapped = self.interner.mapped_type(s_mapped_id);
775        let t_mapped = self.interner.mapped_type(t_mapped_id);
776
777        // Both must have the same constraint (e.g., both `keyof T`)
778        if s_mapped.constraint != t_mapped.constraint {
779            return None;
780        }
781
782        let source_template = s_mapped.template;
783        let mut target_template = t_mapped.template;
784
785        // If the target adds optional (`?`), the target template effectively
786        // becomes `template | undefined` since optional properties accept undefined.
787        let target_adds_optional = t_mapped.optional_modifier == Some(MappedModifier::Add);
788        let source_adds_optional = s_mapped.optional_modifier == Some(MappedModifier::Add);
789
790        if target_adds_optional && !source_adds_optional {
791            target_template = self.interner.union2(target_template, TypeId::UNDEFINED);
792        }
793
794        // If the target removes optional (Required) but source doesn't,
795        // fall through to full structural check.
796        let target_removes_optional = t_mapped.optional_modifier == Some(MappedModifier::Remove);
797        if target_removes_optional && !source_adds_optional && s_mapped.optional_modifier.is_none()
798        {
799            return None;
800        }
801
802        // If both templates are themselves mapped types, recurse
803        if let (Some(s_inner), Some(t_inner)) = (
804            mapped_type_id(self.interner, source_template),
805            mapped_type_id(self.interner, target_template),
806        ) {
807            return self.check_mapped_to_mapped_assignability(s_inner, t_inner);
808        }
809
810        // Compare templates using the subtype checker
811        self.configure_subtype(self.strict_function_types);
812        Some(self.subtype.is_subtype_of(source_template, target_template))
813    }
814
815    /// Check fast-path assignability conditions.
816    /// Returns Some(result) if fast path applies, None if need to do full check.
817    fn check_assignable_fast_path(&self, source: TypeId, target: TypeId) -> Option<bool> {
818        if let Some(TypeData::Lazy(def_id)) = self.interner.lookup(target)
819            && let Some(resolved_target) = self.subtype.resolver.resolve_lazy(def_id, self.interner)
820            && resolved_target != target
821        {
822            return self.check_assignable_fast_path(source, resolved_target);
823        }
824
825        // Same type
826        if source == target {
827            return Some(true);
828        }
829
830        // Any at the top-level is assignable to/from everything
831        // UNLESS strict any propagation is enabled (disables suppression)
832        if source == TypeId::ANY || target == TypeId::ANY {
833            // North Star Fix: any should not silence structural mismatches.
834            // We only allow any to match any here, and fall through to structural
835            // checking for mixed pairs.
836            if source == target {
837                return Some(true);
838            }
839            // If legacy suppression is allowed, we still return true here.
840            if self.lawyer.allow_any_suppression {
841                return Some(true);
842            }
843            // Fall through to structural checking for unsound pairs
844            return None;
845        }
846
847        // Null/undefined in non-strict null check mode
848        if !self.strict_null_checks && source.is_nullish() {
849            return Some(true);
850        }
851
852        // unknown is top
853        if target == TypeId::UNKNOWN {
854            return Some(true);
855        }
856
857        // never is bottom
858        if source == TypeId::NEVER {
859            return Some(true);
860        }
861
862        // Error types are assignable to/from everything (like `any`).
863        // In tsc, errorType silences further errors to prevent cascading diagnostics.
864        if source == TypeId::ERROR || target == TypeId::ERROR {
865            return Some(true);
866        }
867
868        // unknown is not assignable to non-top types
869        if source == TypeId::UNKNOWN {
870            return Some(false);
871        }
872
873        // Compatibility: unions containing `Function` should accept callable sources.
874        // Example: `setTimeout(() => {}, 0)` where first arg is `string | Function`.
875        if let Some(TypeData::Union(members_id)) = self.interner.lookup(target) {
876            let members = self.interner.type_list(members_id);
877            if members
878                .iter()
879                .any(|&member| self.is_function_target_member(member))
880                && crate::type_queries::is_callable_type(self.interner, source)
881            {
882                return Some(true);
883            }
884        }
885
886        None // Need full check
887    }
888
889    pub fn is_assignable_strict(&mut self, source: TypeId, target: TypeId) -> bool {
890        if let Some(TypeData::Lazy(def_id)) = self.interner.lookup(target)
891            && let Some(resolved_target) = self.subtype.resolver.resolve_lazy(def_id, self.interner)
892            && resolved_target != target
893        {
894            return self.is_assignable_strict(source, resolved_target);
895        }
896
897        // Always use strict function types
898        if source == target {
899            return true;
900        }
901        if !self.strict_null_checks && source.is_nullish() {
902            return true;
903        }
904        // Without strictNullChecks, null and undefined are assignable to and from any type.
905        // This check is at the top-level only (not in subtype member iteration).
906        if !self.strict_null_checks && target.is_nullish() {
907            return true;
908        }
909        if target == TypeId::UNKNOWN {
910            return true;
911        }
912        if source == TypeId::NEVER {
913            return true;
914        }
915        // Error types are assignable to/from everything (like `any` in tsc)
916        if source == TypeId::ERROR || target == TypeId::ERROR {
917            return true;
918        }
919        if source == TypeId::UNKNOWN {
920            return false;
921        }
922        if let Some(TypeData::Union(members_id)) = self.interner.lookup(target) {
923            let members = self.interner.type_list(members_id);
924            if members
925                .iter()
926                .any(|&member| self.is_function_target_member(member))
927                && crate::type_queries::is_callable_type(self.interner, source)
928            {
929                return true;
930            }
931        }
932        if self.is_empty_object_target(target) {
933            return self.is_assignable_to_empty_object(source);
934        }
935
936        let prev = self.subtype.strict_function_types;
937        self.configure_subtype(true);
938        let result = self.subtype.is_subtype_of(source, target);
939        self.subtype.strict_function_types = prev;
940        result
941    }
942
943    /// Explain why `source` is not assignable to `target` using TS compatibility rules.
944    pub fn explain_failure(
945        &mut self,
946        source: TypeId,
947        target: TypeId,
948    ) -> Option<SubtypeFailureReason> {
949        // Fast path: if assignable, no failure to explain
950        if source == target {
951            return None;
952        }
953        if target == TypeId::UNKNOWN {
954            return None;
955        }
956        if !self.strict_null_checks && source.is_nullish() {
957            return None;
958        }
959        // Without strictNullChecks, null and undefined are assignable to and from any type.
960        if !self.strict_null_checks && (target == TypeId::NULL || target == TypeId::UNDEFINED) {
961            return None;
962        }
963        if source == TypeId::NEVER {
964            return None;
965        }
966        if source == TypeId::UNKNOWN {
967            return Some(SubtypeFailureReason::TypeMismatch {
968                source_type: source,
969                target_type: target,
970            });
971        }
972
973        // Error types are assignable to/from everything (like `any` in tsc)
974        // No failure to explain — suppress cascading diagnostics
975        if source == TypeId::ERROR || target == TypeId::ERROR {
976            return None;
977        }
978
979        // Weak type violations
980        let violates = self.violates_weak_union(source, target);
981        if violates {
982            return Some(SubtypeFailureReason::TypeMismatch {
983                source_type: source,
984                target_type: target,
985            });
986        }
987        if self.violates_weak_type(source, target) {
988            return Some(SubtypeFailureReason::NoCommonProperties {
989                source_type: source,
990                target_type: target,
991            });
992        }
993
994        // Excess property checking (TS2353)
995        if let Some(excess_prop) = self.find_excess_property(source, target) {
996            return Some(SubtypeFailureReason::ExcessProperty {
997                property_name: excess_prop,
998                target_type: target,
999            });
1000        }
1001
1002        // Private brand incompatibility (TS2322)
1003        // Check this before the structural check so we generate the right error
1004        if let Some(false) = self.private_brand_assignability_override(source, target) {
1005            return Some(SubtypeFailureReason::TypeMismatch {
1006                source_type: source,
1007                target_type: target,
1008            });
1009        }
1010
1011        // Empty object target
1012        if self.is_empty_object_target(target) && self.is_assignable_to_empty_object(source) {
1013            return None;
1014        }
1015
1016        self.configure_subtype(self.strict_function_types);
1017        self.subtype.explain_failure(source, target)
1018    }
1019
1020    const fn configure_subtype(&mut self, strict_function_types: bool) {
1021        self.subtype.strict_function_types = strict_function_types;
1022        self.subtype.allow_void_return = true;
1023        self.subtype.allow_bivariant_rest = true;
1024        self.subtype.exact_optional_property_types = self.exact_optional_property_types;
1025        self.subtype.strict_null_checks = self.strict_null_checks;
1026        self.subtype.no_unchecked_indexed_access = self.no_unchecked_indexed_access;
1027        // Any propagation is controlled by the Lawyer's allow_any_suppression flag
1028        // Standard TypeScript allows any to propagate through arrays/objects regardless
1029        // of strictFunctionTypes - it only affects function parameter variance
1030        self.subtype.any_propagation = self.lawyer.any_propagation_mode();
1031        // In strict mode, disable method bivariance for soundness
1032        self.subtype.disable_method_bivariance = self.strict_subtype_checking;
1033    }
1034
1035    fn violates_weak_type(&self, source: TypeId, target: TypeId) -> bool {
1036        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
1037
1038        let target_shape_id = match extractor.extract(target) {
1039            Some(id) => id,
1040            None => return false,
1041        };
1042
1043        let target_shape = self
1044            .interner
1045            .object_shape(crate::types::ObjectShapeId(target_shape_id));
1046
1047        // ObjectWithIndex with index signatures is not a weak type
1048        if let Some(TypeData::ObjectWithIndex(_)) = self.interner.lookup(target)
1049            && (target_shape.string_index.is_some() || target_shape.number_index.is_some())
1050        {
1051            return false;
1052        }
1053
1054        let target_props = target_shape.properties.as_slice();
1055        if target_props.is_empty() || target_props.iter().any(|prop| !prop.optional) {
1056            return false;
1057        }
1058
1059        self.violates_weak_type_with_target_props(source, target_props)
1060    }
1061
1062    fn violates_weak_union(&self, source: TypeId, target: TypeId) -> bool {
1063        // Don't resolve the target - check it directly for union type
1064        // (resolve_weak_type_ref was converting unions to objects, which is wrong)
1065        let target_key = match self.interner.lookup(target) {
1066            Some(TypeData::Union(members)) => members,
1067            _ => {
1068                return false;
1069            }
1070        };
1071
1072        let members = self.interner.type_list(target_key);
1073        if members.is_empty() {
1074            return false;
1075        }
1076
1077        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
1078        let mut has_weak_member = false;
1079
1080        for member in members.iter() {
1081            let resolved_member = self.resolve_weak_type_ref(*member);
1082            // Weak-union checks only apply when ALL union members are object-like.
1083            // If any member is primitive/non-object (e.g. `string | Function`),
1084            // TypeScript does not apply TS2559-style weak-type rejection.
1085            let member_shape_id = match extractor.extract(resolved_member) {
1086                Some(id) => id,
1087                None => return false,
1088            };
1089
1090            let member_shape = self
1091                .interner
1092                .object_shape(crate::types::ObjectShapeId(member_shape_id));
1093
1094            if member_shape.properties.is_empty()
1095                || member_shape.string_index.is_some()
1096                || member_shape.number_index.is_some()
1097            {
1098                return false;
1099            }
1100
1101            if member_shape.properties.iter().all(|prop| prop.optional) {
1102                has_weak_member = true;
1103            }
1104        }
1105
1106        if !has_weak_member {
1107            return false;
1108        }
1109
1110        self.source_lacks_union_common_property(source, members.as_ref())
1111    }
1112
1113    pub fn is_weak_union_violation(&self, source: TypeId, target: TypeId) -> bool {
1114        self.violates_weak_union(source, target)
1115    }
1116
1117    fn violates_weak_type_with_target_props(
1118        &self,
1119        source: TypeId,
1120        target_props: &[PropertyInfo],
1121    ) -> bool {
1122        // Handle Union types explicitly before visitor
1123        if let Some(TypeData::Union(members)) = self.interner.lookup(source) {
1124            let members = self.interner.type_list(members);
1125            return members
1126                .iter()
1127                .all(|member| self.violates_weak_type_with_target_props(*member, target_props));
1128        }
1129
1130        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
1131        let source_shape_id = match extractor.extract(source) {
1132            Some(id) => id,
1133            None => return false,
1134        };
1135
1136        let source_shape = self
1137            .interner
1138            .object_shape(crate::types::ObjectShapeId(source_shape_id));
1139        let source_props = source_shape.properties.as_slice();
1140
1141        // Empty objects are assignable to weak types (all optional properties).
1142        // Only trigger weak type violation if source has properties that don't overlap.
1143        !source_props.is_empty() && !self.has_common_property(source_props, target_props)
1144    }
1145
1146    fn source_lacks_union_common_property(
1147        &self,
1148        source: TypeId,
1149        target_members: &[TypeId],
1150    ) -> bool {
1151        let source = self.resolve_weak_type_ref(source);
1152
1153        // Handle Union explicitly
1154        if let Some(TypeData::Union(members)) = self.interner.lookup(source) {
1155            let members = self.interner.type_list(members);
1156            return members
1157                .iter()
1158                .all(|member| self.source_lacks_union_common_property(*member, target_members));
1159        }
1160
1161        // Handle TypeParameter explicitly
1162        if let Some(TypeData::TypeParameter(param)) = self.interner.lookup(source) {
1163            return match param.constraint {
1164                Some(constraint) => {
1165                    self.source_lacks_union_common_property(constraint, target_members)
1166                }
1167                None => false,
1168            };
1169        }
1170
1171        // Use visitor for Object types
1172        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
1173        let source_shape_id = match extractor.extract(source) {
1174            Some(id) => id,
1175            None => return false,
1176        };
1177
1178        let source_shape = self
1179            .interner
1180            .object_shape(crate::types::ObjectShapeId(source_shape_id));
1181        if source_shape.string_index.is_some() || source_shape.number_index.is_some() {
1182            return false;
1183        }
1184        let source_props = source_shape.properties.as_slice();
1185        if source_props.is_empty() {
1186            return false;
1187        }
1188
1189        let mut has_common = false;
1190        for member in target_members {
1191            let resolved_member = self.resolve_weak_type_ref(*member);
1192            let member_shape_id = match extractor.extract(resolved_member) {
1193                Some(id) => id,
1194                None => continue,
1195            };
1196
1197            let member_shape = self
1198                .interner
1199                .object_shape(crate::types::ObjectShapeId(member_shape_id));
1200            if member_shape.string_index.is_some() || member_shape.number_index.is_some() {
1201                return false;
1202            }
1203            if self.has_common_property(source_props, member_shape.properties.as_slice()) {
1204                has_common = true;
1205                break;
1206            }
1207        }
1208
1209        !has_common
1210    }
1211
1212    fn has_common_property(
1213        &self,
1214        source_props: &[PropertyInfo],
1215        target_props: &[PropertyInfo],
1216    ) -> bool {
1217        let mut source_idx = 0;
1218        let mut target_idx = 0;
1219
1220        while source_idx < source_props.len() && target_idx < target_props.len() {
1221            let source_name = source_props[source_idx].name;
1222            let target_name = target_props[target_idx].name;
1223            if source_name == target_name {
1224                return true;
1225            }
1226            if source_name < target_name {
1227                source_idx += 1;
1228            } else {
1229                target_idx += 1;
1230            }
1231        }
1232
1233        false
1234    }
1235
1236    fn resolve_weak_type_ref(&self, type_id: TypeId) -> TypeId {
1237        self.subtype.resolve_ref_type(type_id)
1238    }
1239
1240    /// Check if a type is an empty object target.
1241    /// Uses the visitor pattern from `solver::visitor`.
1242    fn is_empty_object_target(&self, target: TypeId) -> bool {
1243        is_empty_object_type_db(self.interner, target)
1244    }
1245
1246    fn is_assignable_to_empty_object(&self, source: TypeId) -> bool {
1247        if source == TypeId::ANY || source == TypeId::NEVER {
1248            return true;
1249        }
1250        // Error types are assignable to everything (like `any` in tsc)
1251        if source == TypeId::ERROR {
1252            return true;
1253        }
1254        if !self.strict_null_checks && source.is_nullish() {
1255            return true;
1256        }
1257        if source == TypeId::UNKNOWN
1258            || source == TypeId::NULL
1259            || source == TypeId::UNDEFINED
1260            || source == TypeId::VOID
1261        {
1262            return false;
1263        }
1264
1265        let key = match self.interner.lookup(source) {
1266            Some(key) => key,
1267            None => return false,
1268        };
1269
1270        match key {
1271            TypeData::Union(members) => {
1272                let members = self.interner.type_list(members);
1273                members
1274                    .iter()
1275                    .all(|member| self.is_assignable_to_empty_object(*member))
1276            }
1277            TypeData::Intersection(members) => {
1278                let members = self.interner.type_list(members);
1279                members
1280                    .iter()
1281                    .any(|member| self.is_assignable_to_empty_object(*member))
1282            }
1283            TypeData::TypeParameter(param) => match param.constraint {
1284                Some(constraint) => self.is_assignable_to_empty_object(constraint),
1285                None => false,
1286            },
1287            _ => true,
1288        }
1289    }
1290}
1291
1292impl<'a, R: TypeResolver> AssignabilityChecker for CompatChecker<'a, R> {
1293    fn is_assignable_to(&mut self, source: TypeId, target: TypeId) -> bool {
1294        self.is_assignable(source, target)
1295    }
1296
1297    fn is_assignable_to_strict(&mut self, source: TypeId, target: TypeId) -> bool {
1298        self.is_assignable_strict(source, target)
1299    }
1300
1301    fn is_assignable_to_bivariant_callback(&mut self, source: TypeId, target: TypeId) -> bool {
1302        // Bypass the cache and perform a one-off check with non-strict function variance.
1303        self.is_assignable_impl(source, target, false)
1304    }
1305
1306    fn evaluate_type(&mut self, type_id: TypeId) -> TypeId {
1307        self.subtype.evaluate_type(type_id)
1308    }
1309}
1310
1311// =============================================================================
1312// Assignability Override Functions (moved from checker/state.rs)
1313// =============================================================================
1314
1315impl<'a, R: TypeResolver> CompatChecker<'a, R> {
1316    /// Check if `source` is assignable to `target` using TS compatibility rules,
1317    /// with checker-provided overrides for enums, abstract constructors, and accessibility.
1318    ///
1319    /// This is the main entry point for assignability checking when checker context is available.
1320    pub fn is_assignable_with_overrides<P: AssignabilityOverrideProvider + ?Sized>(
1321        &mut self,
1322        source: TypeId,
1323        target: TypeId,
1324        overrides: &P,
1325    ) -> bool {
1326        // Check override provider for enum assignability
1327        if let Some(result) = overrides.enum_assignability_override(source, target) {
1328            return result;
1329        }
1330
1331        // Check override provider for abstract constructor assignability
1332        if let Some(result) = overrides.abstract_constructor_assignability_override(source, target)
1333        {
1334            return result;
1335        }
1336
1337        // Check override provider for constructor accessibility
1338        if let Some(result) = overrides.constructor_accessibility_override(source, target) {
1339            return result;
1340        }
1341
1342        // Check private brand assignability (can be done with TypeDatabase alone)
1343        if let Some(result) = self.private_brand_assignability_override(source, target) {
1344            return result;
1345        }
1346
1347        // Fall through to regular assignability check
1348        self.is_assignable(source, target)
1349    }
1350
1351    /// Private brand assignability override.
1352    /// If both source and target types have private brands, they must match exactly.
1353    /// This implements nominal typing for classes with private fields.
1354    ///
1355    /// Uses recursive structure to preserve Union/Intersection semantics:
1356    /// - Union (A | B): OR logic - must satisfy at least one branch
1357    /// - Intersection (A & B): AND logic - must satisfy all branches
1358    pub fn private_brand_assignability_override(
1359        &self,
1360        source: TypeId,
1361        target: TypeId,
1362    ) -> Option<bool> {
1363        use crate::types::Visibility;
1364
1365        // Fast path: identical types don't need nominal brand override logic.
1366        // Let the regular assignability path decide.
1367        if source == target {
1368            return None;
1369        }
1370
1371        // 1. Handle Target Union (OR logic)
1372        // S -> (A | B) : Valid if S -> A OR S -> B
1373        if let Some(TypeData::Union(members)) = self.interner.lookup(target) {
1374            let members = self.interner.type_list(members);
1375            // If source matches ANY target member, it's valid
1376            for &member in members.iter() {
1377                match self.private_brand_assignability_override(source, member) {
1378                    Some(true) | None => return None, // Pass (or structural fallback)
1379                    Some(false) => {}                 // Keep checking other members
1380                }
1381            }
1382            return Some(false); // Failed against all members
1383        }
1384
1385        // 2. Handle Source Union (AND logic)
1386        // (A | B) -> T : Valid if A -> T AND B -> T
1387        if let Some(TypeData::Union(members)) = self.interner.lookup(source) {
1388            let members = self.interner.type_list(members);
1389            for &member in members.iter() {
1390                if let Some(false) = self.private_brand_assignability_override(member, target) {
1391                    return Some(false); // Fail if any member fails
1392                }
1393            }
1394            return None; // All passed or fell back
1395        }
1396
1397        // 3. Handle Target Intersection (AND logic)
1398        // S -> (A & B) : Valid if S -> A AND S -> B
1399        if let Some(TypeData::Intersection(members)) = self.interner.lookup(target) {
1400            let members = self.interner.type_list(members);
1401            for &member in members.iter() {
1402                if let Some(false) = self.private_brand_assignability_override(source, member) {
1403                    return Some(false); // Fail if any member fails
1404                }
1405            }
1406            return None; // All passed or fell back
1407        }
1408
1409        // 4. Handle Source Intersection (OR logic)
1410        // (A & B) -> T : Valid if A -> T OR B -> T
1411        if let Some(TypeData::Intersection(members)) = self.interner.lookup(source) {
1412            let members = self.interner.type_list(members);
1413            for &member in members.iter() {
1414                match self.private_brand_assignability_override(member, target) {
1415                    Some(true) | None => return None, // Pass (or structural fallback)
1416                    Some(false) => {}                 // Keep checking other members
1417                }
1418            }
1419            return Some(false); // Failed against all members
1420        }
1421
1422        // 5. Handle Lazy types (recursive resolution)
1423        if let Some(TypeData::Lazy(def_id)) = self.interner.lookup(source)
1424            && let Some(resolved) = self.subtype.resolver.resolve_lazy(def_id, self.interner)
1425        {
1426            // Guard against non-progressing lazy resolution (e.g. DefId -> same Lazy type),
1427            // which would otherwise recurse forever.
1428            if resolved == source {
1429                return None;
1430            }
1431            return self.private_brand_assignability_override(resolved, target);
1432        }
1433
1434        if let Some(TypeData::Lazy(def_id)) = self.interner.lookup(target)
1435            && let Some(resolved) = self.subtype.resolver.resolve_lazy(def_id, self.interner)
1436        {
1437            // Same non-progress guard for target-side lazy resolution.
1438            if resolved == target {
1439                return None;
1440            }
1441            return self.private_brand_assignability_override(source, resolved);
1442        }
1443
1444        // 6. Base case: Extract and compare object shapes
1445        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
1446
1447        // Get source shape
1448        let source_shape_id = extractor.extract(source)?;
1449        let source_shape = self
1450            .interner
1451            .object_shape(crate::types::ObjectShapeId(source_shape_id));
1452
1453        // Get target shape
1454        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
1455        let target_shape_id = extractor.extract(target)?;
1456        let target_shape = self
1457            .interner
1458            .object_shape(crate::types::ObjectShapeId(target_shape_id));
1459
1460        let mut has_private_brands = false;
1461
1462        // Check Target requirements (Nominality)
1463        // If Target has a private/protected property, Source MUST match its origin exactly.
1464        for target_prop in &target_shape.properties {
1465            if target_prop.visibility == Visibility::Private
1466                || target_prop.visibility == Visibility::Protected
1467            {
1468                has_private_brands = true;
1469                let source_prop = source_shape
1470                    .properties
1471                    .binary_search_by_key(&target_prop.name, |p| p.name)
1472                    .ok()
1473                    .map(|idx| &source_shape.properties[idx]);
1474
1475                match source_prop {
1476                    Some(sp) => {
1477                        // CRITICAL: The parent_id must match exactly.
1478                        if sp.parent_id != target_prop.parent_id {
1479                            return Some(false);
1480                        }
1481                    }
1482                    None => {
1483                        return Some(false);
1484                    }
1485                }
1486            }
1487        }
1488
1489        // Check Source restrictions (Visibility leakage)
1490        // If Source has a private/protected property, it cannot be assigned to a Target
1491        // that expects it to be Public.
1492        for source_prop in &source_shape.properties {
1493            if source_prop.visibility == Visibility::Private
1494                || source_prop.visibility == Visibility::Protected
1495            {
1496                has_private_brands = true;
1497                if let Some(target_prop) = target_shape
1498                    .properties
1499                    .binary_search_by_key(&source_prop.name, |p| p.name)
1500                    .ok()
1501                    .map(|idx| &target_shape.properties[idx])
1502                    && target_prop.visibility == Visibility::Public
1503                {
1504                    return Some(false);
1505                }
1506            }
1507        }
1508
1509        has_private_brands.then_some(true)
1510    }
1511
1512    /// Enum member assignability override.
1513    /// Implements nominal typing for enum members: EnumA.X is NOT assignable to `EnumB` even if values match.
1514    ///
1515    /// TypeScript enum rules:
1516    /// 1. Different enums with different `DefIds` are NOT assignable (nominal typing)
1517    /// 2. Numeric enums are bidirectionally assignable to number (Rule #7 - Open Numeric Enums)
1518    /// 3. String enums are strictly nominal (string literals NOT assignable to string enums)
1519    /// 4. Same enum members with different values are NOT assignable (EnumA.X != EnumA.Y)
1520    /// 5. Unions containing enums: Source union assigned to target enum checks all members
1521    pub fn enum_assignability_override(&self, source: TypeId, target: TypeId) -> Option<bool> {
1522        use crate::type_queries;
1523        use crate::visitor;
1524
1525        // Special case: Source union -> Target enum
1526        // When assigning a union to an enum, ALL enum members in the union must match the target enum.
1527        // This handles cases like: (EnumA | EnumB) assigned to EnumC
1528        // And allows: (Choice.Yes | Choice.No) assigned to Choice (subset of same enum)
1529        if let Some((t_def, _)) = visitor::enum_components(self.interner, target)
1530            && type_queries::is_union_type(self.interner, source)
1531        {
1532            let union_members = type_queries::get_union_members(self.interner, source)?;
1533
1534            let mut all_same_enum = true;
1535            let mut has_non_enum = false;
1536            for &member in &union_members {
1537                if let Some((member_def, _)) = visitor::enum_components(self.interner, member) {
1538                    // Check if this member belongs to the target enum.
1539                    // Members have their own DefIds (different from parent enum's DefId),
1540                    // so we must also check the parent relationship.
1541                    let member_parent = self.subtype.resolver.get_enum_parent_def_id(member_def);
1542                    if member_def != t_def && member_parent != Some(t_def) {
1543                        // Found an enum member from a different enum than target
1544                        return Some(false);
1545                    }
1546                } else {
1547                    all_same_enum = false;
1548                    has_non_enum = true;
1549                }
1550            }
1551
1552            // If ALL union members are enum members from the same enum as the target,
1553            // the union is a subset of the enum and therefore assignable.
1554            // This handles: `type YesNo = Choice.Yes | Choice.No` assignable to `Choice`.
1555            if all_same_enum && !has_non_enum && !union_members.is_empty() {
1556                return Some(true);
1557            }
1558            // Otherwise fall through to structural check for non-enum union members.
1559        }
1560
1561        // String enums are assignable to string (like numeric enums are to number).
1562        // Fall through to structural checking for this case.
1563
1564        // Fast path: Check if both are enum types with same DefId but different TypeIds
1565        // This handles the test case where enum members aren't in the resolver
1566        if let (Some((s_def, _)), Some((t_def, _))) = (
1567            visitor::enum_components(self.interner, source),
1568            visitor::enum_components(self.interner, target),
1569        ) && s_def == t_def
1570            && source != target
1571        {
1572            // Same enum DefId but different TypeIds
1573            // Check if both are literal enum members (not union-based enums)
1574            if self.is_literal_enum_member(source) && self.is_literal_enum_member(target) {
1575                // Both are enum literals with same DefId but different values
1576                // Nominal rule: E.A is NOT assignable to E.B
1577                return Some(false);
1578            }
1579        }
1580
1581        let source_def = self.get_enum_def_id(source);
1582        let target_def = self.get_enum_def_id(target);
1583
1584        match (source_def, target_def) {
1585            // Case 1: Both are enums (or enum members or Union-based enums)
1586            // Note: Same-DefId, different-TypeId case is now handled above before get_enum_def_id
1587            (Some(s_def), Some(t_def)) => {
1588                if s_def == t_def {
1589                    // Same DefId: Same type (E.A -> E.A or E -> E)
1590                    return Some(true);
1591                }
1592
1593                // Gap A: Different DefIds, but might be member -> parent relationship
1594                // Check if they share a parent enum (e.g., E.A -> E)
1595                let s_parent = self.subtype.resolver.get_enum_parent_def_id(s_def);
1596                let t_parent = self.subtype.resolver.get_enum_parent_def_id(t_def);
1597
1598                match (s_parent, t_parent) {
1599                    (Some(sp), Some(tp)) if sp == tp => {
1600                        // Same parent enum
1601                        // If target is the Enum Type (e.g., 'E'), allow structural check
1602                        if self.subtype.resolver.is_enum_type(target, self.interner) {
1603                            return None;
1604                        }
1605                        // If target is a different specific member (e.g., 'E.B'), reject nominally
1606                        // E.A -> E.B should fail even if they have the same value
1607                        Some(false)
1608                    }
1609                    (Some(sp), None) => {
1610                        // Source is a member, target doesn't have a parent (target is not a member)
1611                        // Check if target is the parent enum type
1612                        if t_def == sp {
1613                            // Target is the parent enum of source member
1614                            // Allow member to parent enum assignment (E.A -> E)
1615                            return Some(true);
1616                        }
1617                        // Target is an enum type but not the parent
1618                        Some(false)
1619                    }
1620                    _ => {
1621                        // Different parents (or one/both are types, not members)
1622                        // Nominal mismatch: EnumA.X is not assignable to EnumB
1623                        Some(false)
1624                    }
1625                }
1626            }
1627
1628            // Case 2: Target is an enum, source is a primitive
1629            (None, Some(t_def)) => {
1630                // Check if target is a numeric enum
1631                if self.subtype.resolver.is_numeric_enum(t_def) {
1632                    // Rule #7: Numeric enums allow number assignability
1633                    // BUT we need to distinguish between:
1634                    // - `let x: E = 1` (enum TYPE - allowed)
1635                    // - `let x: E.A = 1` (enum MEMBER - rejected)
1636
1637                    // Check if source is number-like (number or number literal)
1638                    let is_source_number = source == TypeId::NUMBER
1639                        || matches!(
1640                            self.interner.lookup(source),
1641                            Some(TypeData::Literal(LiteralValue::Number(_)))
1642                        );
1643
1644                    if is_source_number {
1645                        // If target is the full Enum Type (e.g., `let x: E = 1`), allow it.
1646                        if self.subtype.resolver.is_enum_type(target, self.interner) {
1647                            return Some(true);
1648                        }
1649
1650                        // If target is a specific member (e.g., `let x: E.A = 1`),
1651                        // fall through to structural check.
1652                        // - `1 -> E.A(0)` will fail structural check (Correct)
1653                        // - `0 -> E.A(0)` will pass structural check (Correct)
1654                        return None;
1655                    }
1656
1657                    None
1658                } else {
1659                    // String enums do NOT allow raw string assignability
1660                    // If source is string or string literal, reject
1661                    if self.is_string_like(source) {
1662                        return Some(false);
1663                    }
1664                    None
1665                }
1666            }
1667
1668            // Case 3: Source is an enum, target is a primitive
1669            // String enums (both types and members) are assignable to string via structural checking
1670            (Some(s_def), None) => {
1671                // Check if source is a string enum
1672                if !self.subtype.resolver.is_numeric_enum(s_def) {
1673                    // Source is a string enum
1674                    if target == TypeId::STRING {
1675                        // Both enum types (Union of members) and enum members (string literals)
1676                        // are assignable to string. Fall through to structural checking.
1677                        return None;
1678                    }
1679                }
1680                // Numeric enums and non-string targets: fall through to structural check
1681                None
1682            }
1683
1684            // Case 4: Neither is an enum
1685            (None, None) => None,
1686        }
1687    }
1688
1689    /// Check if a type is string-like (string, string literal, or template literal).
1690    /// Used to reject primitive-to-string-enum assignments.
1691    fn is_string_like(&self, type_id: TypeId) -> bool {
1692        if type_id == TypeId::STRING {
1693            return true;
1694        }
1695        // Use visitor to check for string literals, template literals, etc.
1696        let mut visitor = StringLikeVisitor { db: self.interner };
1697        visitor.visit_type(self.interner, type_id)
1698    }
1699
1700    /// Get the `DefId` of an enum type, handling both direct Enum members and Union-based Enums.
1701    /// Check whether `type_id` is an enum whose underlying member is a string or number literal.
1702    fn is_literal_enum_member(&self, type_id: TypeId) -> bool {
1703        matches!(
1704            self.interner.lookup(type_id),
1705            Some(TypeData::Enum(_, member_type))
1706                if matches!(
1707                    self.interner.lookup(member_type),
1708                    Some(TypeData::Literal(LiteralValue::Number(_) | LiteralValue::String(_)))
1709                )
1710        )
1711    }
1712
1713    /// Returns `Some(def_id)` if the type is an Enum or a Union of Enum members from the same enum.
1714    /// Returns None if the type is not an enum or contains mixed enums.
1715    fn get_enum_def_id(&self, type_id: TypeId) -> Option<crate::def::DefId> {
1716        use crate::{type_queries, visitor};
1717
1718        // Resolve Lazy types first (handles imported/forward-declared enums)
1719        let resolved =
1720            if let Some(lazy_def_id) = type_queries::get_lazy_def_id(self.interner, type_id) {
1721                // Try to resolve the Lazy type
1722                if let Some(resolved_type) = self
1723                    .subtype
1724                    .resolver
1725                    .resolve_lazy(lazy_def_id, self.interner)
1726                {
1727                    // Guard against self-referential lazy types
1728                    if resolved_type == type_id {
1729                        return None;
1730                    }
1731                    // Recursively check the resolved type
1732                    return self.get_enum_def_id(resolved_type);
1733                }
1734                // Lazy type couldn't be resolved yet, return None
1735                return None;
1736            } else {
1737                type_id
1738            };
1739
1740        // 1. Check for Intrinsic Primitives first (using visitor, not TypeId constants)
1741        // This filters out intrinsic types like string, number, boolean which are stored
1742        // as TypeData::Enum for definition store purposes but are NOT user enums
1743        if visitor::intrinsic_kind(self.interner, resolved).is_some() {
1744            return None;
1745        }
1746
1747        // 2. Check direct Enum member
1748        if let Some((def_id, _inner)) = visitor::enum_components(self.interner, resolved) {
1749            // Use the new is_user_enum_def method to check if this is a user-defined enum
1750            // This properly filters out intrinsic types from lib.d.ts
1751            if self.subtype.resolver.is_user_enum_def(def_id) {
1752                return Some(def_id);
1753            }
1754            // Not a user-defined enum (intrinsic type or type alias)
1755            return None;
1756        }
1757
1758        // 3. Check Union of Enum members (handles Enum types represented as Unions)
1759        if let Some(members) = visitor::union_list_id(self.interner, resolved) {
1760            let members = self.interner.type_list(members);
1761            if members.is_empty() {
1762                return None;
1763            }
1764
1765            let first_def = self.get_enum_def_id(members[0])?;
1766            for &member in members.iter().skip(1) {
1767                if self.get_enum_def_id(member) != Some(first_def) {
1768                    return None; // Mixed union or non-enum members
1769                }
1770            }
1771            return Some(first_def);
1772        }
1773
1774        None
1775    }
1776
1777    /// Checks if two types are compatible for variable redeclaration (TS2403).
1778    ///
1779    /// This applies TypeScript's nominal identity rules for enums and
1780    /// respects 'any' propagation. Used for checking if multiple variable
1781    /// declarations have compatible types.
1782    ///
1783    /// # Examples
1784    /// - `var x: number; var x: number` → true
1785    /// - `var x: E.A; var x: E.A` → true
1786    /// - `var x: E.A; var x: E.B` → false
1787    /// - `var x: E; var x: F` → false (different enums)
1788    /// - `var x: E; var x: number` → false
1789    pub fn are_types_identical_for_redeclaration(&mut self, a: TypeId, b: TypeId) -> bool {
1790        // 1. Fast path: physical identity
1791        if a == b {
1792            return true;
1793        }
1794
1795        // 2. Error propagation — suppress cascading errors from ERROR types.
1796        if a == TypeId::ERROR || b == TypeId::ERROR {
1797            return true;
1798        }
1799
1800        // For redeclaration, `any` is only identical to `any`.
1801        // `a == b` already caught the `any == any` case above.
1802        if a == TypeId::ANY || b == TypeId::ANY {
1803            return false;
1804        }
1805
1806        // 4. Enum Nominality Check
1807        // If one is an enum and the other isn't, or they are different enums,
1808        // they are not identical for redeclaration, even if structurally compatible.
1809        if let Some(res) = self.enum_redeclaration_check(a, b) {
1810            return res;
1811        }
1812
1813        // 5. Normalize Application/Mapped/Lazy types before structural comparison.
1814        // Required<{a?: string}> must evaluate to {a: string} before bidirectional
1815        // subtype checking, just as is_assignable_impl() does via normalize_assignability_operands.
1816        let (a_norm, b_norm) = self.normalize_assignability_operands(a, b);
1817        tracing::trace!(
1818            a = a.0,
1819            b = b.0,
1820            a_norm = a_norm.0,
1821            b_norm = b_norm.0,
1822            a_changed = a != a_norm,
1823            b_changed = b != b_norm,
1824            "are_types_identical_for_redeclaration: normalized"
1825        );
1826        let a = a_norm;
1827        let b = b_norm;
1828
1829        // 5. Structural Identity
1830        // Delegate to the Judge to check bidirectional subtyping
1831        let fwd = self.subtype.is_subtype_of(a, b);
1832        let bwd = self.subtype.is_subtype_of(b, a);
1833        tracing::trace!(
1834            a = a.0,
1835            b = b.0,
1836            fwd,
1837            bwd,
1838            "are_types_identical_for_redeclaration: result"
1839        );
1840        fwd && bwd
1841    }
1842
1843    /// Check if two types involving enums are compatible for redeclaration.
1844    ///
1845    /// Returns Some(bool) if either type is an enum:
1846    /// - Some(false) if different enums or enum vs primitive
1847    /// - None if neither is an enum (delegate to structural check)
1848    fn enum_redeclaration_check(&self, a: TypeId, b: TypeId) -> Option<bool> {
1849        let a_def = self.get_enum_def_id(a);
1850        let b_def = self.get_enum_def_id(b);
1851
1852        match (a_def, b_def) {
1853            (Some(def_a), Some(def_b)) => {
1854                // Both are enums: must be the same enum definition
1855                if def_a != def_b {
1856                    Some(false)
1857                } else {
1858                    // Same enum DefId: compatible for redeclaration
1859                    // This allows: var x: MyEnum; var x = MyEnum.Member;
1860                    // where MyEnum.Member (enum member) is compatible with MyEnum (enum type)
1861                    Some(true)
1862                }
1863            }
1864            (Some(_), None) | (None, Some(_)) => {
1865                // One is an enum, the other is a primitive (e.g., number)
1866                // In TS, Enum E and 'number' are NOT identical for redeclaration
1867                Some(false)
1868            }
1869            (None, None) => None,
1870        }
1871    }
1872}
1873
1874#[cfg(test)]
1875#[path = "../tests/compat_tests.rs"]
1876mod tests;