Skip to main content

tsz_solver/relations/
compat.rs

1//! TypeScript compatibility layer for assignability rules.
2
3use crate::caches::db::QueryDatabase;
4use crate::diagnostics::SubtypeFailureReason;
5use crate::relations::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.
17pub(crate) struct 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    pub(crate) 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    pub(crate) 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).
47pub(crate) struct StringLikeVisitor<'a> {
48    pub(crate) 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        // Can't resolve refs without a resolver, conservatively return false
72        false
73    }
74
75    fn visit_lazy(&mut self, _def_id: u32) -> Self::Output {
76        // We can't resolve Lazy without a resolver, so conservatively return false
77        false
78    }
79
80    fn default_output() -> Self::Output {
81        false
82    }
83}
84
85impl<'a, R: TypeResolver> TypeVisitor for ShapeExtractor<'a, R> {
86    type Output = Option<u32>;
87
88    fn visit_intrinsic(&mut self, _kind: crate::types::IntrinsicKind) -> Self::Output {
89        None
90    }
91
92    fn visit_literal(&mut self, _value: &crate::LiteralValue) -> Self::Output {
93        None
94    }
95
96    fn visit_object(&mut self, shape_id: u32) -> Self::Output {
97        Some(shape_id)
98    }
99
100    fn visit_object_with_index(&mut self, shape_id: u32) -> Self::Output {
101        Some(shape_id)
102    }
103
104    fn visit_lazy(&mut self, def_id: u32) -> Self::Output {
105        let def_id = crate::def::DefId(def_id);
106        if let Some(resolved) = self.resolver.resolve_lazy(def_id, self.db) {
107            return self.extract(resolved);
108        }
109        None
110    }
111
112    fn visit_ref(&mut self, symbol_ref: u32) -> Self::Output {
113        let symbol_ref = crate::types::SymbolRef(symbol_ref);
114        // Prefer DefId resolution if available
115        if let Some(def_id) = self.resolver.symbol_to_def_id(symbol_ref) {
116            return self.visit_lazy(def_id.0);
117        }
118        if let Some(resolved) = self.resolver.resolve_symbol_ref(symbol_ref, self.db) {
119            return self.extract(resolved);
120        }
121        None
122    }
123
124    // TSZ-4: Handle Intersection types for nominal checking
125    // For private brands, we need to find object shapes within the intersection
126    fn visit_intersection(&mut self, list_id: u32) -> Self::Output {
127        let member_list = self.db.type_list(crate::types::TypeListId(list_id));
128        // For nominal checking, iterate and return the first valid object shape found
129        // This ensures we check the private/protected members of constituent types
130        for member in member_list.iter() {
131            if let Some(shape) = self.visit_type(self.db, *member) {
132                return Some(shape);
133            }
134        }
135        None
136    }
137
138    fn default_output() -> Self::Output {
139        None
140    }
141}
142
143/// Trait for providing checker-specific assignability overrides.
144///
145/// This allows the solver's `CompatChecker` to call back into the checker
146/// for special cases that require binder/symbol information (enums,
147/// abstract constructors, constructor accessibility).
148pub trait AssignabilityOverrideProvider {
149    /// Override for enum assignability rules.
150    /// Returns Some(true/false) if the override applies, None to fall through to structural checking.
151    fn enum_assignability_override(&self, source: TypeId, target: TypeId) -> Option<bool>;
152
153    /// Override for abstract constructor assignability rules.
154    /// Returns Some(false) if abstract class cannot be assigned to concrete constructor, None otherwise.
155    fn abstract_constructor_assignability_override(
156        &self,
157        source: TypeId,
158        target: TypeId,
159    ) -> Option<bool>;
160
161    /// Override for constructor accessibility rules (private/protected).
162    /// Returns Some(false) if accessibility mismatch prevents assignment, None otherwise.
163    fn constructor_accessibility_override(&self, source: TypeId, target: TypeId) -> Option<bool>;
164}
165
166/// A no-op implementation of `AssignabilityOverrideProvider` for when no checker context is available.
167pub struct NoopOverrideProvider;
168
169impl AssignabilityOverrideProvider for NoopOverrideProvider {
170    fn enum_assignability_override(&self, _source: TypeId, _target: TypeId) -> Option<bool> {
171        None
172    }
173
174    fn abstract_constructor_assignability_override(
175        &self,
176        _source: TypeId,
177        _target: TypeId,
178    ) -> Option<bool> {
179        None
180    }
181
182    fn constructor_accessibility_override(&self, _source: TypeId, _target: TypeId) -> Option<bool> {
183        None
184    }
185}
186
187/// Compatibility checker that applies TypeScript's unsound rules
188/// before delegating to the structural subtype engine.
189///
190/// This layer integrates with the "Lawyer" layer to apply nuanced rules
191/// for `any` propagation.
192pub struct CompatChecker<'a, R: TypeResolver = NoopResolver> {
193    pub(crate) interner: &'a dyn TypeDatabase,
194    /// Optional query database for Salsa-backed memoization.
195    query_db: Option<&'a dyn QueryDatabase>,
196    pub(crate) subtype: SubtypeChecker<'a, R>,
197    /// The "Lawyer" layer - handles nuanced rules for `any` propagation.
198    lawyer: AnyPropagationRules,
199    strict_function_types: bool,
200    strict_null_checks: bool,
201    no_unchecked_indexed_access: bool,
202    exact_optional_property_types: bool,
203    /// When true, enables additional strict subtype checking rules for lib.d.ts
204    strict_subtype_checking: bool,
205    cache: FxHashMap<(TypeId, TypeId), bool>,
206}
207
208impl<'a> CompatChecker<'a, NoopResolver> {
209    /// Create a new compatibility checker without a resolver.
210    /// Note: Callers should configure `strict_function_types` explicitly via `set_strict_function_types()`
211    pub fn new(interner: &'a dyn TypeDatabase) -> Self {
212        CompatChecker {
213            interner,
214            query_db: None,
215            subtype: SubtypeChecker::new(interner),
216            lawyer: AnyPropagationRules::new(),
217            // Default to false (legacy TypeScript behavior) for compatibility
218            // Callers should set this explicitly based on compiler options
219            strict_function_types: false,
220            strict_null_checks: true,
221            no_unchecked_indexed_access: false,
222            exact_optional_property_types: false,
223            strict_subtype_checking: false,
224            cache: FxHashMap::default(),
225        }
226    }
227}
228
229impl<'a, R: TypeResolver> CompatChecker<'a, R> {
230    fn normalize_assignability_operand(&mut self, mut type_id: TypeId) -> TypeId {
231        // Keep normalization bounded to avoid infinite resolver/evaluator cycles.
232        for _ in 0..8 {
233            let next = match self.interner.lookup(type_id) {
234                Some(TypeData::Lazy(def_id)) => self
235                    .subtype
236                    .resolver
237                    .resolve_lazy(def_id, self.interner)
238                    .unwrap_or(type_id),
239                Some(TypeData::Mapped(_) | TypeData::Application(_)) => {
240                    self.subtype.evaluate_type(type_id)
241                }
242                _ => type_id,
243            };
244
245            if next == type_id {
246                break;
247            }
248            type_id = next;
249        }
250        type_id
251    }
252
253    pub(crate) fn normalize_assignability_operands(
254        &mut self,
255        source: TypeId,
256        target: TypeId,
257    ) -> (TypeId, TypeId) {
258        (
259            self.normalize_assignability_operand(source),
260            self.normalize_assignability_operand(target),
261        )
262    }
263
264    fn is_function_target_member(&self, member: TypeId) -> bool {
265        let is_function_object_shape = match self.interner.lookup(member) {
266            Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
267                let shape = self.interner.object_shape(shape_id);
268                let apply = self.interner.intern_string("apply");
269                let call = self.interner.intern_string("call");
270                let has_apply = shape.properties.iter().any(|prop| prop.name == apply);
271                let has_call = shape.properties.iter().any(|prop| prop.name == call);
272                has_apply && has_call
273            }
274            _ => false,
275        };
276
277        intrinsic_kind(self.interner, member) == Some(IntrinsicKind::Function)
278            || is_function_object_shape
279            || self
280                .subtype
281                .resolver
282                .get_boxed_type(IntrinsicKind::Function)
283                .is_some_and(|boxed| boxed == member)
284            || lazy_def_id(self.interner, member).is_some_and(|def_id| {
285                self.subtype
286                    .resolver
287                    .is_boxed_def_id(def_id, IntrinsicKind::Function)
288            })
289    }
290
291    /// Create a new compatibility checker with a resolver.
292    /// Note: Callers should configure `strict_function_types` explicitly via `set_strict_function_types()`
293    pub fn with_resolver(interner: &'a dyn TypeDatabase, resolver: &'a R) -> Self {
294        CompatChecker {
295            interner,
296            query_db: None,
297            subtype: SubtypeChecker::with_resolver(interner, resolver),
298            lawyer: AnyPropagationRules::new(),
299            // Default to false (legacy TypeScript behavior) for compatibility
300            // Callers should set this explicitly based on compiler options
301            strict_function_types: false,
302            strict_null_checks: true,
303            no_unchecked_indexed_access: false,
304            exact_optional_property_types: false,
305            strict_subtype_checking: false,
306            cache: FxHashMap::default(),
307        }
308    }
309
310    /// Set the query database for Salsa-backed memoization.
311    /// Propagates to the internal `SubtypeChecker`.
312    pub fn set_query_db(&mut self, db: &'a dyn QueryDatabase) {
313        self.query_db = Some(db);
314        self.subtype.query_db = Some(db);
315    }
316
317    /// Set the inheritance graph for nominal class subtype checking.
318    /// Propagates to the internal `SubtypeChecker`.
319    pub const fn set_inheritance_graph(
320        &mut self,
321        graph: Option<&'a crate::classes::inheritance::InheritanceGraph>,
322    ) {
323        self.subtype.inheritance_graph = graph;
324    }
325
326    /// Configure strict function parameter checking.
327    /// See <https://github.com/microsoft/TypeScript/issues/18654>.
328    pub fn set_strict_function_types(&mut self, strict: bool) {
329        if self.strict_function_types != strict {
330            self.strict_function_types = strict;
331            self.cache.clear();
332        }
333    }
334
335    /// Configure strict null checks (legacy null/undefined assignability).
336    pub fn set_strict_null_checks(&mut self, strict: bool) {
337        if self.strict_null_checks != strict {
338            self.strict_null_checks = strict;
339            self.cache.clear();
340        }
341    }
342
343    /// Configure unchecked indexed access (include `undefined` in `T[K]`).
344    pub fn set_no_unchecked_indexed_access(&mut self, enabled: bool) {
345        if self.no_unchecked_indexed_access != enabled {
346            self.no_unchecked_indexed_access = enabled;
347            self.cache.clear();
348        }
349    }
350
351    /// Configure exact optional property types.
352    /// See <https://github.com/microsoft/TypeScript/issues/13195>.
353    pub fn set_exact_optional_property_types(&mut self, exact: bool) {
354        if self.exact_optional_property_types != exact {
355            self.exact_optional_property_types = exact;
356            self.cache.clear();
357        }
358    }
359
360    /// Configure strict mode for `any` propagation.
361    /// Configure strict subtype checking mode for lib.d.ts type checking.
362    ///
363    /// When enabled, applies additional strictness rules that reject borderline
364    /// cases allowed by TypeScript's legacy behavior. This includes disabling
365    /// method bivariance for soundness.
366    pub fn set_strict_subtype_checking(&mut self, strict: bool) {
367        if self.strict_subtype_checking != strict {
368            self.strict_subtype_checking = strict;
369            self.cache.clear();
370        }
371    }
372
373    /// Apply compiler options from a bitmask flags value.
374    ///
375    /// The flags correspond to `RelationCacheKey` bits:
376    /// - bit 0: `strict_null_checks`
377    /// - bit 1: `strict_function_types`
378    /// - bit 2: `exact_optional_property_types`
379    /// - bit 3: `no_unchecked_indexed_access`
380    /// - bit 4: `disable_method_bivariance` (`strict_subtype_checking`)
381    /// - bit 5: `allow_void_return`
382    /// - bit 6: `allow_bivariant_rest`
383    /// - bit 7: `allow_bivariant_param_count`
384    ///
385    /// This is used by `QueryCache::is_assignable_to_with_flags` to ensure
386    /// cached results respect the compiler configuration.
387    pub fn apply_flags(&mut self, flags: u16) {
388        // Apply flags to CompatChecker's own fields
389        let strict_null_checks = (flags & (1 << 0)) != 0;
390        let strict_function_types = (flags & (1 << 1)) != 0;
391        let exact_optional_property_types = (flags & (1 << 2)) != 0;
392        let no_unchecked_indexed_access = (flags & (1 << 3)) != 0;
393        let disable_method_bivariance = (flags & (1 << 4)) != 0;
394
395        self.set_strict_null_checks(strict_null_checks);
396        self.set_strict_function_types(strict_function_types);
397        self.set_exact_optional_property_types(exact_optional_property_types);
398        self.set_no_unchecked_indexed_access(no_unchecked_indexed_access);
399        self.set_strict_subtype_checking(disable_method_bivariance);
400
401        // Also apply flags to the internal SubtypeChecker
402        // We do this directly since apply_flags() uses a builder pattern
403        self.subtype.strict_null_checks = strict_null_checks;
404        self.subtype.strict_function_types = strict_function_types;
405        self.subtype.exact_optional_property_types = exact_optional_property_types;
406        self.subtype.no_unchecked_indexed_access = no_unchecked_indexed_access;
407        self.subtype.disable_method_bivariance = disable_method_bivariance;
408        self.subtype.allow_void_return = (flags & (1 << 5)) != 0;
409        self.subtype.allow_bivariant_rest = (flags & (1 << 6)) != 0;
410        self.subtype.allow_bivariant_param_count = (flags & (1 << 7)) != 0;
411    }
412
413    ///
414    /// When strict mode is enabled, `any` does NOT silence structural mismatches.
415    /// This means the type checker will still report errors even when `any` is involved,
416    /// if there's a real structural mismatch.
417    pub fn set_strict_any_propagation(&mut self, strict: bool) {
418        self.lawyer.set_allow_any_suppression(!strict);
419        self.cache.clear();
420    }
421
422    /// Get a reference to the lawyer layer for `any` propagation rules.
423    pub const fn lawyer(&self) -> &AnyPropagationRules {
424        &self.lawyer
425    }
426
427    /// Get a mutable reference to the lawyer layer for `any` propagation rules.
428    pub fn lawyer_mut(&mut self) -> &mut AnyPropagationRules {
429        self.cache.clear();
430        &mut self.lawyer
431    }
432
433    /// Apply configuration from `JudgeConfig`.
434    ///
435    /// This is used to configure the `CompatChecker` with settings from
436    /// the `CompilerOptions` (passed through `JudgeConfig`).
437    pub fn apply_config(&mut self, config: &crate::judge::JudgeConfig) {
438        self.strict_function_types = config.strict_function_types;
439        self.strict_null_checks = config.strict_null_checks;
440        self.exact_optional_property_types = config.exact_optional_property_types;
441        self.no_unchecked_indexed_access = config.no_unchecked_indexed_access;
442
443        // North Star: any should NOT silence structural mismatches in strict mode
444        self.lawyer.allow_any_suppression = !config.strict_function_types && !config.sound_mode;
445
446        // Clear cache as configuration changed
447        self.cache.clear();
448    }
449
450    /// Check if `source` is assignable to `target` using TS compatibility rules.
451    pub fn is_assignable(&mut self, source: TypeId, target: TypeId) -> bool {
452        // Without strictNullChecks, null and undefined are assignable to and from any type.
453        // This check is at the top-level only (not in subtype member iteration) to avoid
454        // incorrectly accepting types within union member comparisons.
455        if !self.strict_null_checks && target.is_nullish() {
456            return true;
457        }
458
459        let key = (source, target);
460        if let Some(&cached) = self.cache.get(&key) {
461            return cached;
462        }
463
464        let result = self.is_assignable_impl(source, target, self.strict_function_types);
465
466        self.cache.insert(key, result);
467        result
468    }
469
470    /// Check for excess properties in object literal assignment (TS2353).
471    ///
472    /// This implements the "Lawyer" layer rule where fresh object literals
473    /// cannot have properties that don't exist in the target type, unless the
474    /// target has an index signature.
475    ///
476    /// # Arguments
477    /// * `source` - The source type (should be a fresh object literal)
478    /// * `target` - The target type
479    ///
480    /// # Returns
481    /// `true` if no excess properties found, `false` if TS2353 should be reported
482    fn check_excess_properties(&mut self, source: TypeId, target: TypeId) -> bool {
483        use super::freshness::is_fresh_object_type;
484        use crate::visitor::{ObjectTypeKind, classify_object_type};
485
486        // Only check fresh object literals
487        if !is_fresh_object_type(self.interner, source) {
488            return true;
489        }
490
491        // Get source shape
492        let source_shape_id = match classify_object_type(self.interner, source) {
493            ObjectTypeKind::Object(shape_id) | ObjectTypeKind::ObjectWithIndex(shape_id) => {
494                shape_id
495            }
496            ObjectTypeKind::NotObject => return true,
497        };
498
499        let source_shape = self.interner.object_shape(source_shape_id);
500
501        let (has_string_index, has_number_index) = self.check_index_signatures(target);
502
503        // If target has string index signature, skip excess property check entirely
504        if has_string_index {
505            return true;
506        }
507
508        // Collect all target properties (including base types if intersection)
509        let target_properties = self.collect_target_properties(target);
510
511        // TypeScript forgives excess properties when the target type is completely empty
512        // (like `{}`, an empty interface, or an empty class) because it accepts any non-primitive.
513        if target_properties.is_empty() && !has_number_index {
514            return true;
515        }
516
517        // Check each source property
518        for prop_info in &source_shape.properties {
519            if !target_properties.contains(&prop_info.name) {
520                // If target has a numeric index signature, numeric-named properties are allowed
521                if has_number_index {
522                    let name_str = self.interner.resolve_atom(prop_info.name);
523                    if name_str.parse::<f64>().is_ok() {
524                        continue;
525                    }
526                }
527                // Excess property found!
528                return false;
529            }
530        }
531
532        true
533    }
534
535    /// Find the first excess property in object literal assignment.
536    ///
537    /// Returns `Some(property_name)` if an excess property is found, `None` otherwise.
538    /// This is used by `explain_failure` to generate TS2353 diagnostics.
539    fn find_excess_property(&mut self, source: TypeId, target: TypeId) -> Option<Atom> {
540        use super::freshness::is_fresh_object_type;
541        use crate::visitor::{ObjectTypeKind, classify_object_type};
542
543        // Only check fresh object literals
544        if !is_fresh_object_type(self.interner, source) {
545            return None;
546        }
547
548        // Get source shape
549        let source_shape_id = match classify_object_type(self.interner, source) {
550            ObjectTypeKind::Object(shape_id) | ObjectTypeKind::ObjectWithIndex(shape_id) => {
551                shape_id
552            }
553            ObjectTypeKind::NotObject => return None,
554        };
555
556        let source_shape = self.interner.object_shape(source_shape_id);
557
558        // Get target shape - resolve Lazy, Mapped, and Application types
559        let target_key = self.interner.lookup(target);
560        let resolved_target = match target_key {
561            Some(TypeData::Lazy(def_id)) => {
562                // Try to resolve the Lazy type
563                self.subtype.resolver.resolve_lazy(def_id, self.interner)?
564            }
565            Some(TypeData::Mapped(_) | TypeData::Application(_)) => {
566                // Evaluate mapped and application types
567                self.subtype.evaluate_type(target)
568            }
569            _ => target,
570        };
571
572        let (has_string_index, has_number_index) = self.check_index_signatures(resolved_target);
573
574        // If target has string index signature, skip excess property check entirely
575        if has_string_index {
576            return None;
577        }
578
579        // Collect all target properties (including base types if intersection)
580        let target_properties = self.collect_target_properties(resolved_target);
581
582        // TypeScript forgives excess properties when the target type is completely empty
583        if target_properties.is_empty() && !has_number_index {
584            return None;
585        }
586
587        // Check each source property
588        for prop_info in &source_shape.properties {
589            if !target_properties.contains(&prop_info.name) {
590                // If target has a numeric index signature, numeric-named properties are allowed
591                if has_number_index {
592                    let name_str = self.interner.resolve_atom(prop_info.name);
593                    if name_str.parse::<f64>().is_ok() {
594                        continue;
595                    }
596                }
597                // Excess property found!
598                return Some(prop_info.name);
599            }
600        }
601
602        None
603    }
604
605    /// Collect all property names from a type into a set (handles intersections and unions).
606    ///
607    /// For intersections: property exists if it's in ANY member
608    /// For unions: property exists if it's in ALL members
609    /// Check if a type or any of its composite members has a string or numeric index signature.
610    /// Returns `(has_string_index, has_number_index)`.
611    fn check_index_signatures(&mut self, type_id: TypeId) -> (bool, bool) {
612        if type_id == TypeId::ANY || type_id == TypeId::UNKNOWN || type_id == TypeId::ERROR {
613            return (true, true);
614        }
615
616        let type_id = match self.interner.lookup(type_id) {
617            Some(TypeData::Lazy(def_id)) => self
618                .subtype
619                .resolver
620                .resolve_lazy(def_id, self.interner)
621                .unwrap_or(type_id),
622            Some(TypeData::Mapped(_) | TypeData::Application(_)) => {
623                self.subtype.evaluate_type(type_id)
624            }
625            _ => type_id,
626        };
627
628        if type_id == TypeId::ANY || type_id == TypeId::UNKNOWN || type_id == TypeId::ERROR {
629            return (true, true);
630        }
631
632        match self.interner.lookup(type_id) {
633            Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
634                let shape = self.interner.object_shape(shape_id);
635                (shape.string_index.is_some(), shape.number_index.is_some())
636            }
637            Some(TypeData::Intersection(members_id)) | Some(TypeData::Union(members_id)) => {
638                let members = self.interner.type_list(members_id);
639                let mut has_str = false;
640                let mut has_num = false;
641                for &member in members.iter() {
642                    let (s, n) = self.check_index_signatures(member);
643                    has_str |= s;
644                    has_num |= n;
645                }
646                (has_str, has_num)
647            }
648            _ => (false, false),
649        }
650    }
651
652    fn collect_target_properties(&mut self, type_id: TypeId) -> rustc_hash::FxHashSet<Atom> {
653        // Handle Mapped and Application types by evaluating them to concrete types
654        // We resolve before matching so the existing logic handles the result.
655        let type_id = match self.interner.lookup(type_id) {
656            Some(TypeData::Mapped(_) | TypeData::Application(_)) => {
657                self.subtype.evaluate_type(type_id)
658            }
659            _ => type_id,
660        };
661
662        let mut properties = rustc_hash::FxHashSet::default();
663
664        match self.interner.lookup(type_id) {
665            Some(TypeData::Intersection(members_id)) => {
666                let members = self.interner.type_list(members_id);
667                // Property exists if it's in ANY member of intersection
668                for &member in members.iter() {
669                    let member_props = self.collect_target_properties(member);
670                    properties.extend(member_props);
671                }
672            }
673            Some(TypeData::Union(members_id)) => {
674                let members = self.interner.type_list(members_id);
675                if members.is_empty() {
676                    return properties;
677                }
678                // For unions, property exists if it's in ALL members
679                // Start with first member's properties
680                let mut all_props = self.collect_target_properties(members[0]);
681                // Intersect with remaining members
682                for &member in members.iter().skip(1) {
683                    let member_props = self.collect_target_properties(member);
684                    all_props = all_props.intersection(&member_props).cloned().collect();
685                }
686                properties = all_props;
687            }
688            Some(TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id)) => {
689                let shape = self.interner.object_shape(shape_id);
690                for prop_info in &shape.properties {
691                    properties.insert(prop_info.name);
692                }
693            }
694            _ => {}
695        }
696
697        properties
698    }
699
700    /// Internal implementation of assignability check.
701    /// Extracted to share logic between `is_assignable` and `is_assignable_strict`.
702    fn is_assignable_impl(
703        &mut self,
704        source: TypeId,
705        target: TypeId,
706        strict_function_types: bool,
707    ) -> bool {
708        let (source, target) = self.normalize_assignability_operands(source, target);
709
710        // Fast path checks
711        if let Some(result) = self.check_assignable_fast_path(source, target) {
712            return result;
713        }
714
715        // Enum nominal typing check (Lawyer layer implementation)
716        // This provides enum member distinction even without checker context
717        if let Some(result) = self.enum_assignability_override(source, target) {
718            return result;
719        }
720
721        // Weak type checks
722        if self.violates_weak_union(source, target) {
723            return false;
724        }
725        if self.violates_weak_type(source, target) {
726            return false;
727        }
728
729        // Excess property checking (TS2353) - Lawyer layer
730        if !self.check_excess_properties(source, target) {
731            return false;
732        }
733
734        // Empty object target
735        if self.is_empty_object_target(target) {
736            return self.is_assignable_to_empty_object(source);
737        }
738
739        // Check mapped-to-mapped structural comparison before full subtype check.
740        // When both source and target are deferred mapped types over the same constraint
741        // (e.g., Readonly<T> vs Partial<T>), compare template types directly.
742        if let (Some(TypeData::Mapped(s_mapped_id)), Some(TypeData::Mapped(t_mapped_id))) =
743            (self.interner.lookup(source), self.interner.lookup(target))
744        {
745            let result = self.check_mapped_to_mapped_assignability(s_mapped_id, t_mapped_id);
746            if let Some(assignable) = result {
747                return assignable;
748            }
749        }
750
751        // Default to structural subtype checking
752        self.configure_subtype(strict_function_types);
753        self.subtype.is_subtype_of(source, target)
754    }
755
756    /// Check if two mapped types are assignable via structural template comparison.
757    ///
758    /// When both source and target are mapped types with the same constraint
759    /// (e.g., both iterate over `keyof T`), compare their templates directly.
760    /// This handles cases like `Readonly<T>` assignable to `Partial<T>` where
761    /// the mapped types can't be concretely expanded because T is generic.
762    ///
763    /// Returns `Some(true/false)` if determination was made, `None` to fall through.
764    fn check_mapped_to_mapped_assignability(
765        &mut self,
766        s_mapped_id: crate::types::MappedTypeId,
767        t_mapped_id: crate::types::MappedTypeId,
768    ) -> Option<bool> {
769        use crate::types::MappedModifier;
770        use crate::visitor::mapped_type_id;
771
772        let s_mapped = self.interner.mapped_type(s_mapped_id);
773        let t_mapped = self.interner.mapped_type(t_mapped_id);
774
775        // Both must have the same constraint (e.g., both `keyof T`)
776        if s_mapped.constraint != t_mapped.constraint {
777            return None;
778        }
779
780        let source_template = s_mapped.template;
781        let mut target_template = t_mapped.template;
782
783        // If the target adds optional (`?`), the target template effectively
784        // becomes `template | undefined` since optional properties accept undefined.
785        let target_adds_optional = t_mapped.optional_modifier == Some(MappedModifier::Add);
786        let source_adds_optional = s_mapped.optional_modifier == Some(MappedModifier::Add);
787
788        if target_adds_optional && !source_adds_optional {
789            target_template = self.interner.union2(target_template, TypeId::UNDEFINED);
790        }
791
792        // If the target removes optional (Required) but source doesn't,
793        // fall through to full structural check.
794        let target_removes_optional = t_mapped.optional_modifier == Some(MappedModifier::Remove);
795        if target_removes_optional && !source_adds_optional && s_mapped.optional_modifier.is_none()
796        {
797            return None;
798        }
799
800        // If both templates are themselves mapped types, recurse
801        if let (Some(s_inner), Some(t_inner)) = (
802            mapped_type_id(self.interner, source_template),
803            mapped_type_id(self.interner, target_template),
804        ) {
805            return self.check_mapped_to_mapped_assignability(s_inner, t_inner);
806        }
807
808        // Compare templates using the subtype checker
809        self.configure_subtype(self.strict_function_types);
810        Some(self.subtype.is_subtype_of(source_template, target_template))
811    }
812
813    /// Check fast-path assignability conditions.
814    /// Returns Some(result) if fast path applies, None if need to do full check.
815    fn check_assignable_fast_path(&self, source: TypeId, target: TypeId) -> Option<bool> {
816        if let Some(TypeData::Lazy(def_id)) = self.interner.lookup(target)
817            && let Some(resolved_target) = self.subtype.resolver.resolve_lazy(def_id, self.interner)
818            && resolved_target != target
819        {
820            return self.check_assignable_fast_path(source, resolved_target);
821        }
822
823        // Same type
824        if source == target {
825            return Some(true);
826        }
827
828        // Any at the top-level is assignable to/from everything
829        // UNLESS strict any propagation is enabled (disables suppression)
830        if source == TypeId::ANY || target == TypeId::ANY {
831            // North Star Fix: any should not silence structural mismatches.
832            // We only allow any to match any here, and fall through to structural
833            // checking for mixed pairs.
834            if source == target {
835                return Some(true);
836            }
837            // If legacy suppression is allowed, we still return true here.
838            if self.lawyer.allow_any_suppression {
839                return Some(true);
840            }
841            // Fall through to structural checking for unsound pairs
842            return None;
843        }
844
845        // Null/undefined in non-strict null check mode
846        if !self.strict_null_checks && source.is_nullish() {
847            return Some(true);
848        }
849
850        // unknown is top
851        if target == TypeId::UNKNOWN {
852            return Some(true);
853        }
854
855        // never is bottom
856        if source == TypeId::NEVER {
857            return Some(true);
858        }
859
860        // Error types are assignable to/from everything (like `any`).
861        // In tsc, errorType silences further errors to prevent cascading diagnostics.
862        if source == TypeId::ERROR || target == TypeId::ERROR {
863            return Some(true);
864        }
865
866        // unknown is not assignable to non-top types
867        if source == TypeId::UNKNOWN {
868            return Some(false);
869        }
870
871        // Compatibility: unions containing `Function` should accept callable sources.
872        // Example: `setTimeout(() => {}, 0)` where first arg is `string | Function`.
873        if let Some(TypeData::Union(members_id)) = self.interner.lookup(target) {
874            let members = self.interner.type_list(members_id);
875            if members
876                .iter()
877                .any(|&member| self.is_function_target_member(member))
878                && crate::type_queries::is_callable_type(self.interner, source)
879            {
880                return Some(true);
881            }
882        }
883
884        None // Need full check
885    }
886
887    pub fn is_assignable_strict(&mut self, source: TypeId, target: TypeId) -> bool {
888        if let Some(TypeData::Lazy(def_id)) = self.interner.lookup(target)
889            && let Some(resolved_target) = self.subtype.resolver.resolve_lazy(def_id, self.interner)
890            && resolved_target != target
891        {
892            return self.is_assignable_strict(source, resolved_target);
893        }
894
895        // Always use strict function types
896        if source == target {
897            return true;
898        }
899        if !self.strict_null_checks && source.is_nullish() {
900            return true;
901        }
902        // Without strictNullChecks, null and undefined are assignable to and from any type.
903        // This check is at the top-level only (not in subtype member iteration).
904        if !self.strict_null_checks && target.is_nullish() {
905            return true;
906        }
907        if target == TypeId::UNKNOWN {
908            return true;
909        }
910        if source == TypeId::NEVER {
911            return true;
912        }
913        // Error types are assignable to/from everything (like `any` in tsc)
914        if source == TypeId::ERROR || target == TypeId::ERROR {
915            return true;
916        }
917        if source == TypeId::UNKNOWN {
918            return false;
919        }
920        if let Some(TypeData::Union(members_id)) = self.interner.lookup(target) {
921            let members = self.interner.type_list(members_id);
922            if members
923                .iter()
924                .any(|&member| self.is_function_target_member(member))
925                && crate::type_queries::is_callable_type(self.interner, source)
926            {
927                return true;
928            }
929        }
930        if self.is_empty_object_target(target) {
931            return self.is_assignable_to_empty_object(source);
932        }
933
934        let prev = self.subtype.strict_function_types;
935        self.configure_subtype(true);
936        let result = self.subtype.is_subtype_of(source, target);
937        self.subtype.strict_function_types = prev;
938        result
939    }
940
941    /// Explain why `source` is not assignable to `target` using TS compatibility rules.
942    pub fn explain_failure(
943        &mut self,
944        source: TypeId,
945        target: TypeId,
946    ) -> Option<SubtypeFailureReason> {
947        // Fast path: if assignable, no failure to explain
948        if source == target {
949            return None;
950        }
951        if target == TypeId::UNKNOWN {
952            return None;
953        }
954        if !self.strict_null_checks && source.is_nullish() {
955            return None;
956        }
957        // Without strictNullChecks, null and undefined are assignable to and from any type.
958        if !self.strict_null_checks && (target == TypeId::NULL || target == TypeId::UNDEFINED) {
959            return None;
960        }
961        if source == TypeId::NEVER {
962            return None;
963        }
964        if source == TypeId::UNKNOWN {
965            return Some(SubtypeFailureReason::TypeMismatch {
966                source_type: source,
967                target_type: target,
968            });
969        }
970
971        // Error types are assignable to/from everything (like `any` in tsc)
972        // No failure to explain — suppress cascading diagnostics
973        if source == TypeId::ERROR || target == TypeId::ERROR {
974            return None;
975        }
976
977        // Weak type violations
978        let violates = self.violates_weak_union(source, target);
979        if violates {
980            return Some(SubtypeFailureReason::TypeMismatch {
981                source_type: source,
982                target_type: target,
983            });
984        }
985        if self.violates_weak_type(source, target) {
986            return Some(SubtypeFailureReason::NoCommonProperties {
987                source_type: source,
988                target_type: target,
989            });
990        }
991
992        // Excess property checking (TS2353)
993        if let Some(excess_prop) = self.find_excess_property(source, target) {
994            return Some(SubtypeFailureReason::ExcessProperty {
995                property_name: excess_prop,
996                target_type: target,
997            });
998        }
999
1000        // Private brand incompatibility (TS2322)
1001        // Check this before the structural check so we generate the right error
1002        if let Some(false) = self.private_brand_assignability_override(source, target) {
1003            return Some(SubtypeFailureReason::TypeMismatch {
1004                source_type: source,
1005                target_type: target,
1006            });
1007        }
1008
1009        // Empty object target
1010        if self.is_empty_object_target(target) && self.is_assignable_to_empty_object(source) {
1011            return None;
1012        }
1013
1014        self.configure_subtype(self.strict_function_types);
1015        self.subtype.explain_failure(source, target)
1016    }
1017
1018    const fn configure_subtype(&mut self, strict_function_types: bool) {
1019        self.subtype.strict_function_types = strict_function_types;
1020        self.subtype.allow_void_return = true;
1021        self.subtype.allow_bivariant_rest = true;
1022        self.subtype.exact_optional_property_types = self.exact_optional_property_types;
1023        self.subtype.strict_null_checks = self.strict_null_checks;
1024        self.subtype.no_unchecked_indexed_access = self.no_unchecked_indexed_access;
1025        // Any propagation is controlled by the Lawyer's allow_any_suppression flag
1026        // Standard TypeScript allows any to propagate through arrays/objects regardless
1027        // of strictFunctionTypes - it only affects function parameter variance
1028        self.subtype.any_propagation = self.lawyer.any_propagation_mode();
1029        // In strict mode, disable method bivariance for soundness
1030        self.subtype.disable_method_bivariance = self.strict_subtype_checking;
1031    }
1032
1033    fn violates_weak_type(&self, source: TypeId, target: TypeId) -> bool {
1034        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
1035
1036        let target_shape_id = match extractor.extract(target) {
1037            Some(id) => id,
1038            None => return false,
1039        };
1040
1041        let target_shape = self
1042            .interner
1043            .object_shape(crate::types::ObjectShapeId(target_shape_id));
1044
1045        // ObjectWithIndex with index signatures is not a weak type
1046        if let Some(TypeData::ObjectWithIndex(_)) = self.interner.lookup(target)
1047            && (target_shape.string_index.is_some() || target_shape.number_index.is_some())
1048        {
1049            return false;
1050        }
1051
1052        let target_props = target_shape.properties.as_slice();
1053        if target_props.is_empty() || target_props.iter().any(|prop| !prop.optional) {
1054            return false;
1055        }
1056
1057        self.violates_weak_type_with_target_props(source, target_props)
1058    }
1059
1060    fn violates_weak_union(&self, source: TypeId, target: TypeId) -> bool {
1061        // Don't resolve the target - check it directly for union type
1062        // (resolve_weak_type_ref was converting unions to objects, which is wrong)
1063        let target_key = match self.interner.lookup(target) {
1064            Some(TypeData::Union(members)) => members,
1065            _ => {
1066                return false;
1067            }
1068        };
1069
1070        let members = self.interner.type_list(target_key);
1071        if members.is_empty() {
1072            return false;
1073        }
1074
1075        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
1076        let mut has_weak_member = false;
1077
1078        for member in members.iter() {
1079            let resolved_member = self.resolve_weak_type_ref(*member);
1080            // Weak-union checks only apply when ALL union members are object-like.
1081            // If any member is primitive/non-object (e.g. `string | Function`),
1082            // TypeScript does not apply TS2559-style weak-type rejection.
1083            let member_shape_id = match extractor.extract(resolved_member) {
1084                Some(id) => id,
1085                None => return false,
1086            };
1087
1088            let member_shape = self
1089                .interner
1090                .object_shape(crate::types::ObjectShapeId(member_shape_id));
1091
1092            if member_shape.properties.is_empty()
1093                || member_shape.string_index.is_some()
1094                || member_shape.number_index.is_some()
1095            {
1096                return false;
1097            }
1098
1099            if member_shape.properties.iter().all(|prop| prop.optional) {
1100                has_weak_member = true;
1101            }
1102        }
1103
1104        if !has_weak_member {
1105            return false;
1106        }
1107
1108        self.source_lacks_union_common_property(source, members.as_ref())
1109    }
1110
1111    pub fn is_weak_union_violation(&self, source: TypeId, target: TypeId) -> bool {
1112        self.violates_weak_union(source, target)
1113    }
1114
1115    fn violates_weak_type_with_target_props(
1116        &self,
1117        source: TypeId,
1118        target_props: &[PropertyInfo],
1119    ) -> bool {
1120        // Handle Union types explicitly before visitor
1121        if let Some(TypeData::Union(members)) = self.interner.lookup(source) {
1122            let members = self.interner.type_list(members);
1123            return members
1124                .iter()
1125                .all(|member| self.violates_weak_type_with_target_props(*member, target_props));
1126        }
1127
1128        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
1129        let source_shape_id = match extractor.extract(source) {
1130            Some(id) => id,
1131            None => return false,
1132        };
1133
1134        let source_shape = self
1135            .interner
1136            .object_shape(crate::types::ObjectShapeId(source_shape_id));
1137        let source_props = source_shape.properties.as_slice();
1138
1139        // Empty objects are assignable to weak types (all optional properties).
1140        // Only trigger weak type violation if source has properties that don't overlap.
1141        !source_props.is_empty() && !self.has_common_property(source_props, target_props)
1142    }
1143
1144    fn source_lacks_union_common_property(
1145        &self,
1146        source: TypeId,
1147        target_members: &[TypeId],
1148    ) -> bool {
1149        let source = self.resolve_weak_type_ref(source);
1150
1151        // Handle Union explicitly
1152        if let Some(TypeData::Union(members)) = self.interner.lookup(source) {
1153            let members = self.interner.type_list(members);
1154            return members
1155                .iter()
1156                .all(|member| self.source_lacks_union_common_property(*member, target_members));
1157        }
1158
1159        // Handle TypeParameter explicitly
1160        if let Some(TypeData::TypeParameter(param)) = self.interner.lookup(source) {
1161            return match param.constraint {
1162                Some(constraint) => {
1163                    self.source_lacks_union_common_property(constraint, target_members)
1164                }
1165                None => false,
1166            };
1167        }
1168
1169        // Use visitor for Object types
1170        let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
1171        let source_shape_id = match extractor.extract(source) {
1172            Some(id) => id,
1173            None => return false,
1174        };
1175
1176        let source_shape = self
1177            .interner
1178            .object_shape(crate::types::ObjectShapeId(source_shape_id));
1179        if source_shape.string_index.is_some() || source_shape.number_index.is_some() {
1180            return false;
1181        }
1182        let source_props = source_shape.properties.as_slice();
1183        if source_props.is_empty() {
1184            return false;
1185        }
1186
1187        let mut has_common = false;
1188        for member in target_members {
1189            let resolved_member = self.resolve_weak_type_ref(*member);
1190            let member_shape_id = match extractor.extract(resolved_member) {
1191                Some(id) => id,
1192                None => continue,
1193            };
1194
1195            let member_shape = self
1196                .interner
1197                .object_shape(crate::types::ObjectShapeId(member_shape_id));
1198            if member_shape.string_index.is_some() || member_shape.number_index.is_some() {
1199                return false;
1200            }
1201            if self.has_common_property(source_props, member_shape.properties.as_slice()) {
1202                has_common = true;
1203                break;
1204            }
1205        }
1206
1207        !has_common
1208    }
1209
1210    fn has_common_property(
1211        &self,
1212        source_props: &[PropertyInfo],
1213        target_props: &[PropertyInfo],
1214    ) -> bool {
1215        let mut source_idx = 0;
1216        let mut target_idx = 0;
1217
1218        while source_idx < source_props.len() && target_idx < target_props.len() {
1219            let source_name = source_props[source_idx].name;
1220            let target_name = target_props[target_idx].name;
1221            if source_name == target_name {
1222                return true;
1223            }
1224            if source_name < target_name {
1225                source_idx += 1;
1226            } else {
1227                target_idx += 1;
1228            }
1229        }
1230
1231        false
1232    }
1233
1234    fn resolve_weak_type_ref(&self, type_id: TypeId) -> TypeId {
1235        self.subtype.resolve_ref_type(type_id)
1236    }
1237
1238    /// Check if a type is an empty object target.
1239    /// Uses the visitor pattern from `solver::visitor`.
1240    fn is_empty_object_target(&self, target: TypeId) -> bool {
1241        is_empty_object_type_db(self.interner, target)
1242    }
1243
1244    fn is_assignable_to_empty_object(&self, source: TypeId) -> bool {
1245        if source == TypeId::ANY || source == TypeId::NEVER {
1246            return true;
1247        }
1248        // Error types are assignable to everything (like `any` in tsc)
1249        if source == TypeId::ERROR {
1250            return true;
1251        }
1252        if !self.strict_null_checks && source.is_nullish() {
1253            return true;
1254        }
1255        if source == TypeId::UNKNOWN
1256            || source == TypeId::NULL
1257            || source == TypeId::UNDEFINED
1258            || source == TypeId::VOID
1259        {
1260            return false;
1261        }
1262
1263        let key = match self.interner.lookup(source) {
1264            Some(key) => key,
1265            None => return false,
1266        };
1267
1268        match key {
1269            TypeData::Union(members) => {
1270                let members = self.interner.type_list(members);
1271                members
1272                    .iter()
1273                    .all(|member| self.is_assignable_to_empty_object(*member))
1274            }
1275            TypeData::Intersection(members) => {
1276                let members = self.interner.type_list(members);
1277                members
1278                    .iter()
1279                    .any(|member| self.is_assignable_to_empty_object(*member))
1280            }
1281            TypeData::TypeParameter(param) => match param.constraint {
1282                Some(constraint) => self.is_assignable_to_empty_object(constraint),
1283                None => false,
1284            },
1285            _ => true,
1286        }
1287    }
1288}
1289
1290impl<'a, R: TypeResolver> AssignabilityChecker for CompatChecker<'a, R> {
1291    fn is_assignable_to(&mut self, source: TypeId, target: TypeId) -> bool {
1292        self.is_assignable(source, target)
1293    }
1294
1295    fn is_assignable_to_strict(&mut self, source: TypeId, target: TypeId) -> bool {
1296        self.is_assignable_strict(source, target)
1297    }
1298
1299    fn is_assignable_to_bivariant_callback(&mut self, source: TypeId, target: TypeId) -> bool {
1300        // Bypass the cache and perform a one-off check with non-strict function variance.
1301        self.is_assignable_impl(source, target, false)
1302    }
1303
1304    fn evaluate_type(&mut self, type_id: TypeId) -> TypeId {
1305        self.subtype.evaluate_type(type_id)
1306    }
1307}
1308
1309#[cfg(test)]
1310#[path = "../../tests/compat_tests.rs"]
1311mod tests;