Skip to main content

tsz_checker/assignability/
assignability_checker.rs

1//! Type assignability and excess property checking.
2//!
3//! Subtype, identity, and redeclaration compatibility live in
4//! `subtype_identity_checker`.
5
6use crate::query_boundaries::assignability::{
7    AssignabilityEvalKind, AssignabilityQueryInputs, ExcessPropertiesKind,
8    are_types_overlapping_with_env, check_assignable_gate_with_overrides,
9    classify_for_assignability_eval, classify_for_excess_properties,
10    is_assignable_bivariant_with_resolver, is_assignable_with_overrides, is_callable_type,
11    is_relation_cacheable, object_shape_for_type,
12};
13use crate::state::{CheckerOverrideProvider, CheckerState};
14use rustc_hash::FxHashSet;
15use tracing::trace;
16use tsz_common::interner::Atom;
17use tsz_parser::parser::NodeIndex;
18use tsz_parser::parser::node::NodeAccess;
19use tsz_parser::parser::node_flags;
20use tsz_parser::parser::syntax_kind_ext;
21use tsz_scanner::SyntaxKind;
22use tsz_solver::NarrowingContext;
23use tsz_solver::RelationCacheKey;
24use tsz_solver::TypeId;
25use tsz_solver::visitor::{collect_lazy_def_ids, collect_type_queries};
26
27// =============================================================================
28// Assignability Checking Methods
29// =============================================================================
30
31impl<'a> CheckerState<'a> {
32    fn get_keyof_type_keys(
33        &mut self,
34        type_id: TypeId,
35        db: &dyn tsz_solver::TypeDatabase,
36    ) -> FxHashSet<Atom> {
37        if let Some(keyof_type) = tsz_solver::type_queries::get_keyof_type(db, type_id)
38            && let Some(key_type) =
39                tsz_solver::type_queries::keyof_object_properties(db, keyof_type)
40            && let Some(members) = tsz_solver::type_queries::get_union_members(db, key_type)
41        {
42            return members
43                .into_iter()
44                .filter_map(|m| {
45                    if let Some(str_lit) = tsz_solver::type_queries::get_string_literal_value(db, m)
46                    {
47                        return Some(str_lit);
48                    }
49                    None
50                })
51                .collect();
52        }
53        FxHashSet::default()
54    }
55
56    fn typeof_this_comparison_literal(
57        &self,
58        left: NodeIndex,
59        right: NodeIndex,
60        this_ref: NodeIndex,
61    ) -> Option<&str> {
62        if self.is_typeof_this_target(left, this_ref) {
63            return self.string_literal_text(right);
64        }
65        if self.is_typeof_this_target(right, this_ref) {
66            return self.string_literal_text(left);
67        }
68        None
69    }
70
71    fn is_typeof_this_target(&self, expr: NodeIndex, this_ref: NodeIndex) -> bool {
72        let expr = self.ctx.arena.skip_parenthesized(expr);
73        let Some(node) = self.ctx.arena.get(expr) else {
74            return false;
75        };
76        if node.kind != syntax_kind_ext::PREFIX_UNARY_EXPRESSION {
77            return false;
78        }
79        let Some(unary) = self.ctx.arena.get_unary_expr(node) else {
80            return false;
81        };
82        if unary.operator != SyntaxKind::TypeOfKeyword as u16 {
83            return false;
84        }
85        let operand = self.ctx.arena.skip_parenthesized(unary.operand);
86        if operand == this_ref {
87            return true;
88        }
89        self.ctx
90            .arena
91            .get(operand)
92            .is_some_and(|n| n.kind == SyntaxKind::ThisKeyword as u16)
93    }
94
95    fn string_literal_text(&self, idx: NodeIndex) -> Option<&str> {
96        let idx = self.ctx.arena.skip_parenthesized(idx);
97        let node = self.ctx.arena.get(idx)?;
98        if node.kind == SyntaxKind::StringLiteral as u16
99            || node.kind == SyntaxKind::NoSubstitutionTemplateLiteral as u16
100        {
101            return self
102                .ctx
103                .arena
104                .get_literal(node)
105                .map(|lit| lit.text.as_str());
106        }
107        None
108    }
109
110    fn narrow_this_from_enclosing_typeof_guard(
111        &self,
112        source_idx: NodeIndex,
113        source: TypeId,
114    ) -> TypeId {
115        let is_this_source = self
116            .ctx
117            .arena
118            .get(source_idx)
119            .is_some_and(|n| n.kind == SyntaxKind::ThisKeyword as u16);
120        if !is_this_source {
121            return source;
122        }
123
124        let mut current = source_idx;
125        let mut depth = 0usize;
126        while depth < 256 {
127            depth += 1;
128            let Some(ext) = self.ctx.arena.get_extended(current) else {
129                break;
130            };
131            if ext.parent.is_none() {
132                break;
133            }
134            current = ext.parent;
135            let Some(parent_node) = self.ctx.arena.get(current) else {
136                break;
137            };
138            if parent_node.kind != syntax_kind_ext::IF_STATEMENT {
139                continue;
140            }
141            let Some(if_stmt) = self.ctx.arena.get_if_statement(parent_node) else {
142                continue;
143            };
144            if !self.is_node_within(source_idx, if_stmt.then_statement) {
145                continue;
146            }
147            let Some(cond_node) = self.ctx.arena.get(if_stmt.expression) else {
148                continue;
149            };
150            if cond_node.kind != syntax_kind_ext::BINARY_EXPRESSION {
151                continue;
152            }
153            let Some(bin) = self.ctx.arena.get_binary_expr(cond_node) else {
154                continue;
155            };
156            let is_equality = bin.operator_token == SyntaxKind::EqualsEqualsEqualsToken as u16
157                || bin.operator_token == SyntaxKind::EqualsEqualsToken as u16;
158            if !is_equality {
159                continue;
160            }
161            if let Some(type_name) =
162                self.typeof_this_comparison_literal(bin.left, bin.right, source_idx)
163            {
164                return NarrowingContext::new(self.ctx.types).narrow_by_typeof(source, type_name);
165            }
166        }
167
168        source
169    }
170
171    /// Ensure relation preconditions (lazy refs + application symbols) for one type.
172    pub(crate) fn ensure_relation_input_ready(&mut self, type_id: TypeId) {
173        self.ensure_refs_resolved(type_id);
174        self.ensure_application_symbols_resolved(type_id);
175    }
176
177    /// Ensure relation preconditions (lazy refs + application symbols) for multiple types.
178    pub(crate) fn ensure_relation_inputs_ready(&mut self, type_ids: &[TypeId]) {
179        for &type_id in type_ids {
180            self.ensure_relation_input_ready(type_id);
181        }
182    }
183
184    /// Centralized suppression for TS2322-style assignability diagnostics.
185    pub(crate) const fn should_suppress_assignability_diagnostic(
186        &self,
187        source: TypeId,
188        target: TypeId,
189    ) -> bool {
190        matches!(source, TypeId::ERROR | TypeId::ANY)
191            || matches!(target, TypeId::ERROR | TypeId::ANY)
192    }
193
194    /// Suppress assignability diagnostics when they are likely parser-recovery artifacts.
195    ///
196    /// In files with real syntax errors, we often get placeholder nodes and transient
197    /// parse states. Checker-level semantics should not emit TS2322 there.
198    fn should_suppress_assignability_for_parse_recovery(
199        &self,
200        source_idx: NodeIndex,
201        diag_idx: NodeIndex,
202    ) -> bool {
203        if !self.has_syntax_parse_errors() {
204            return false;
205        }
206
207        if self.ctx.syntax_parse_error_positions.is_empty() {
208            return false;
209        }
210
211        self.is_parse_recovery_anchor_node(source_idx)
212            || self.is_parse_recovery_anchor_node(diag_idx)
213    }
214
215    /// Detect nodes that look like parser-recovery artifacts.
216    ///
217    /// Recovery heuristics:
218    /// - Missing-expression placeholders are currently identifiers with empty text.
219    /// - Nodes that start very near a syntax parse error are considered unstable.
220    /// - Nodes in subtrees that were marked as parse-recovery by the parser are suppressed.
221    fn is_parse_recovery_anchor_node(&self, idx: NodeIndex) -> bool {
222        let Some(node) = self.ctx.arena.get(idx) else {
223            return false;
224        };
225
226        // Missing-expression placeholders used by parser recovery.
227        if self
228            .ctx
229            .arena
230            .get_identifier_text(idx)
231            .is_some_and(str::is_empty)
232        {
233            return true;
234        }
235
236        // Also suppress diagnostics anchored very near a syntax parse error.
237        const DIAG_PARSE_DISTANCE: u32 = 16;
238        for &err_pos in &self.ctx.syntax_parse_error_positions {
239            let before = err_pos.saturating_sub(DIAG_PARSE_DISTANCE);
240            let after = err_pos.saturating_add(DIAG_PARSE_DISTANCE);
241            if (node.pos >= before && node.pos <= after)
242                || (node.end >= before && node.end <= after)
243            {
244                return true;
245            }
246        }
247
248        let mut current = idx;
249        let mut walk_guard = 0;
250        while current.is_some() {
251            walk_guard += 1;
252            if walk_guard > 512 {
253                break;
254            }
255
256            if let Some(current_node) = self.ctx.arena.get(current) {
257                let flags = current_node.flags as u32;
258                if (flags & node_flags::THIS_NODE_HAS_ERROR) != 0
259                    || (flags & node_flags::THIS_NODE_OR_ANY_SUB_NODES_HAS_ERROR) != 0
260                {
261                    return true;
262                }
263            } else {
264                break;
265            }
266
267            let Some(ext) = self.ctx.arena.get_extended(current) else {
268                break;
269            };
270            if ext.parent.is_none() {
271                break;
272            }
273            current = ext.parent;
274        }
275
276        false
277    }
278
279    // =========================================================================
280    // Type Evaluation for Assignability
281    // =========================================================================
282
283    /// Ensure all Ref types in a type are resolved and in the type environment.
284    ///
285    /// This is critical for intersection/union type assignability. When we have
286    /// `type AB = A & B`, the intersection contains Ref(A) and Ref(B). Before we
287    /// can check assignability against the intersection, we need to ensure A and B
288    /// are resolved and in `type_env` so the subtype checker can resolve them.
289    pub(crate) fn ensure_refs_resolved(&mut self, type_id: TypeId) {
290        let mut visited_types = FxHashSet::default();
291        let mut visited_def_ids = FxHashSet::default();
292        let mut worklist = vec![type_id];
293
294        while let Some(current) = worklist.pop() {
295            if !visited_types.insert(current) {
296                continue;
297            }
298
299            for symbol_ref in collect_type_queries(self.ctx.types, current) {
300                let sym_id = tsz_binder::SymbolId(symbol_ref.0);
301                let _ = self.get_type_of_symbol(sym_id);
302            }
303
304            for def_id in collect_lazy_def_ids(self.ctx.types, current) {
305                if !visited_def_ids.insert(def_id) {
306                    continue;
307                }
308                if let Some(result) = self.resolve_and_insert_def_type(def_id)
309                    && result != TypeId::ERROR
310                    && result != TypeId::ANY
311                {
312                    worklist.push(result);
313                }
314            }
315        }
316    }
317
318    /// Evaluate a type for assignability checking.
319    ///
320    /// Determines if the type needs evaluation (applications, env-dependent types)
321    /// and performs the appropriate evaluation.
322    pub(crate) fn evaluate_type_for_assignability(&mut self, type_id: TypeId) -> TypeId {
323        let mut evaluated = match classify_for_assignability_eval(self.ctx.types, type_id) {
324            AssignabilityEvalKind::Application => self.evaluate_type_with_resolution(type_id),
325            AssignabilityEvalKind::NeedsEnvEval => self.evaluate_type_with_env(type_id),
326            AssignabilityEvalKind::Resolved => type_id,
327        };
328
329        // Distribution pass: normalize compound types so mixed representations do not
330        // leak into relation checks (for example, `Lazy(Class)` + resolved class object).
331        if let Some(distributed) =
332            tsz_solver::type_queries::map_compound_members(self.ctx.types, evaluated, |member| {
333                self.evaluate_type_for_assignability(member)
334            })
335        {
336            evaluated = distributed;
337        }
338
339        evaluated
340    }
341
342    // =========================================================================
343    // Main Assignability Check
344    // =========================================================================
345
346    /// Substitute `ThisType` in a type with the enclosing class instance type.
347    ///
348    /// When inside a class body, `ThisType` represents the polymorphic `this` type
349    /// (a type parameter bounded by the class). Since the `this` expression evaluates
350    /// to the concrete class instance type, we must substitute `ThisType` → class
351    /// instance type before assignability checks. This matches tsc's behavior where
352    /// `return this`, `f(this)`, etc. succeed when the target type is `this`.
353    fn substitute_this_type_if_needed(&mut self, type_id: TypeId) -> TypeId {
354        // Fast path: intrinsic types can't contain ThisType
355        if type_id.is_intrinsic() {
356            return type_id;
357        }
358
359        let needs_substitution = tsz_solver::is_this_type(self.ctx.types, type_id);
360
361        if !needs_substitution {
362            return type_id;
363        }
364
365        let Some(class_info) = &self.ctx.enclosing_class else {
366            return type_id;
367        };
368        let class_idx = class_info.class_idx;
369
370        let Some(node) = self.ctx.arena.get(class_idx) else {
371            return type_id;
372        };
373        let Some(class_data) = self.ctx.arena.get_class(node) else {
374            return type_id;
375        };
376
377        let instance_type = self.get_class_instance_type(class_idx, class_data);
378
379        if tsz_solver::is_this_type(self.ctx.types, type_id) {
380            instance_type
381        } else {
382            tsz_solver::substitute_this_type(self.ctx.types, type_id, instance_type)
383        }
384    }
385
386    /// Check if source type is assignable to target type.
387    ///
388    /// This is the main entry point for assignability checking, used throughout
389    /// the type system to validate assignments, function calls, returns, etc.
390    /// Assignability is more permissive than subtyping.
391    pub fn is_assignable_to(&mut self, source: TypeId, target: TypeId) -> bool {
392        // CRITICAL: Ensure all Ref types are resolved before assignability check.
393        // This fixes intersection type assignability where `type AB = A & B` needs
394        // A and B in type_env before we can check if a type is assignable to the intersection.
395        self.ensure_relation_input_ready(target);
396
397        // Substitute `ThisType` in the target with the class instance type.
398        // In tsc, `this` acts as a type parameter constrained to the class type.
399        // The `this` expression evaluates to the concrete class instance type, so when
400        // the target (return type, parameter type, etc.) contains `ThisType`, we need to
401        // resolve it to the class instance type before the assignability check.
402        let target = self.substitute_this_type_if_needed(target);
403
404        // Pre-check: Function interface accepts any callable type.
405        // Must check before evaluate_type_for_assignability resolves Lazy(DefId)
406        // to ObjectShape, losing the DefId identity needed to recognize it as Function.
407        {
408            use tsz_solver::visitor::lazy_def_id;
409            let is_function_target = lazy_def_id(self.ctx.types, target).is_some_and(|t_def| {
410                self.ctx.type_env.try_borrow().ok().is_some_and(|env| {
411                    env.is_boxed_def_id(t_def, tsz_solver::IntrinsicKind::Function)
412                })
413            });
414            if is_function_target {
415                let source_eval = self.evaluate_type_for_assignability(source);
416                if is_callable_type(self.ctx.types, source_eval) {
417                    return true;
418                }
419            }
420        }
421
422        let source = self.evaluate_type_for_assignability(source);
423        let target = self.evaluate_type_for_assignability(target);
424
425        // Check relation cache for non-inference types
426        // Construct RelationCacheKey with Lawyer-layer flags to prevent cache poisoning
427        // Note: Use ORIGINAL types for cache key, not evaluated types
428        let is_cacheable = is_relation_cacheable(self.ctx.types, source, target);
429
430        let flags = self.ctx.pack_relation_flags();
431
432        if is_cacheable {
433            let cache_key = RelationCacheKey::assignability(source, target, flags, 0);
434
435            if let Some(cached) = self.ctx.types.lookup_assignability_cache(cache_key) {
436                return cached;
437            }
438        }
439
440        // Use CheckerContext as the resolver instead of TypeEnvironment
441        // This enables access to symbol information for enum type detection
442        let overrides = CheckerOverrideProvider::new(self, None);
443        let result = is_assignable_with_overrides(
444            &AssignabilityQueryInputs {
445                db: self.ctx.types,
446                resolver: &self.ctx,
447                source,
448                target,
449                flags,
450                inheritance_graph: &self.ctx.inheritance_graph,
451                sound_mode: self.ctx.sound_mode(),
452            },
453            &overrides,
454        );
455
456        if is_cacheable {
457            let cache_key = RelationCacheKey::assignability(source, target, flags, 0);
458
459            self.ctx.types.insert_assignability_cache(cache_key, result);
460        }
461
462        trace!(
463            source = source.0,
464            target = target.0,
465            result,
466            "is_assignable_to"
467        );
468
469        // Add keyof type checking logic
470        if let Some(keyof_type) = tsz_solver::type_queries::get_keyof_type(self.ctx.types, target)
471            && let Some(source_atom) =
472                tsz_solver::type_queries::get_string_literal_value(self.ctx.types, source)
473        {
474            let source_str = self.ctx.types.resolve_atom(source_atom);
475            let allowed_keys =
476                tsz_solver::type_queries::get_allowed_keys(self.ctx.types, keyof_type);
477            if !allowed_keys.contains(&source_str) {
478                return false;
479            }
480        }
481
482        result
483    }
484
485    ///
486    /// This keeps the same checker gateway (resolver + overrides + caches) as
487    /// `is_assignable_to`, but forces the strict-function-types relation flag.
488    pub fn is_assignable_to_strict(&mut self, source: TypeId, target: TypeId) -> bool {
489        self.ensure_relation_input_ready(target);
490
491        let target = self.substitute_this_type_if_needed(target);
492        let source = self.evaluate_type_for_assignability(source);
493        let target = self.evaluate_type_for_assignability(target);
494
495        let is_cacheable = is_relation_cacheable(self.ctx.types, source, target);
496        let flags = self.ctx.pack_relation_flags() | RelationCacheKey::FLAG_STRICT_FUNCTION_TYPES;
497
498        if is_cacheable {
499            let cache_key = RelationCacheKey::assignability(source, target, flags, 0);
500            if let Some(cached) = self.ctx.types.lookup_assignability_cache(cache_key) {
501                return cached;
502            }
503        }
504
505        let overrides = CheckerOverrideProvider::new(self, None);
506        let result = is_assignable_with_overrides(
507            &AssignabilityQueryInputs {
508                db: self.ctx.types,
509                resolver: &self.ctx,
510                source,
511                target,
512                flags,
513                inheritance_graph: &self.ctx.inheritance_graph,
514                sound_mode: self.ctx.sound_mode(),
515            },
516            &overrides,
517        );
518
519        if is_cacheable {
520            let cache_key = RelationCacheKey::assignability(source, target, flags, 0);
521            self.ctx.types.insert_assignability_cache(cache_key, result);
522        }
523
524        trace!(
525            source = source.0,
526            target = target.0,
527            result,
528            "is_assignable_to_strict"
529        );
530        result
531    }
532
533    /// Check assignability while forcing strict null checks in relation flags.
534    ///
535    /// This keeps the regular checker/solver assignability gateway (resolver,
536    /// overrides, caching, and precondition setup) while pinning nullability
537    /// semantics to strict mode for localized checks.
538    pub fn is_assignable_to_strict_null(&mut self, source: TypeId, target: TypeId) -> bool {
539        self.ensure_relation_input_ready(target);
540
541        let target = self.substitute_this_type_if_needed(target);
542        let source = self.evaluate_type_for_assignability(source);
543        let target = self.evaluate_type_for_assignability(target);
544
545        let is_cacheable = is_relation_cacheable(self.ctx.types, source, target);
546        let flags = self.ctx.pack_relation_flags() | RelationCacheKey::FLAG_STRICT_NULL_CHECKS;
547
548        if is_cacheable {
549            let cache_key = RelationCacheKey::assignability(source, target, flags, 0);
550            if let Some(cached) = self.ctx.types.lookup_assignability_cache(cache_key) {
551                return cached;
552            }
553        }
554
555        let overrides = CheckerOverrideProvider::new(self, None);
556        let result = is_assignable_with_overrides(
557            &AssignabilityQueryInputs {
558                db: self.ctx.types,
559                resolver: &self.ctx,
560                source,
561                target,
562                flags,
563                inheritance_graph: &self.ctx.inheritance_graph,
564                sound_mode: self.ctx.sound_mode(),
565            },
566            &overrides,
567        );
568
569        if is_cacheable {
570            let cache_key = RelationCacheKey::assignability(source, target, flags, 0);
571            self.ctx.types.insert_assignability_cache(cache_key, result);
572        }
573
574        trace!(
575            source = source.0,
576            target = target.0,
577            result,
578            "is_assignable_to_strict_null"
579        );
580        result
581    }
582
583    /// Check if `source` type is assignable to `target` type, resolving Ref types.
584    ///
585    /// Uses the provided `TypeEnvironment` to resolve type references.
586    pub fn is_assignable_to_with_env(
587        &self,
588        source: TypeId,
589        target: TypeId,
590        env: &tsz_solver::TypeEnvironment,
591    ) -> bool {
592        let flags = self.ctx.pack_relation_flags();
593        let overrides = CheckerOverrideProvider::new(self, Some(env));
594        is_assignable_with_overrides(
595            &AssignabilityQueryInputs {
596                db: self.ctx.types,
597                resolver: env,
598                source,
599                target,
600                flags,
601                inheritance_graph: &self.ctx.inheritance_graph,
602                sound_mode: self.ctx.sound_mode(),
603            },
604            &overrides,
605        )
606    }
607
608    /// Check if `source` type is assignable to `target` type with bivariant function parameter checking.
609    ///
610    /// This is used for class method override checking, where methods are always bivariant
611    /// (unlike function properties which are contravariant with strictFunctionTypes).
612    ///
613    /// Follows the same pattern as `is_assignable_to` but calls `is_assignable_to_bivariant_callback`
614    /// which disables `strict_function_types` for the check.
615    pub fn is_assignable_to_bivariant(&mut self, source: TypeId, target: TypeId) -> bool {
616        // CRITICAL: Ensure all Ref types are resolved before assignability check.
617        // This fixes intersection type assignability where `type AB = A & B` needs
618        // A and B in type_env before we can check if a type is assignable to the intersection.
619        self.ensure_relation_input_ready(target);
620
621        let source = self.evaluate_type_for_assignability(source);
622        let target = self.evaluate_type_for_assignability(target);
623
624        // Check relation cache for non-inference types
625        // Construct RelationCacheKey with Lawyer-layer flags to prevent cache poisoning
626        // Note: Use ORIGINAL types for cache key, not evaluated types
627        let is_cacheable = is_relation_cacheable(self.ctx.types, source, target);
628
629        // For bivariant checks, we strip the strict_function_types flag
630        // so the cache key is distinct from regular assignability checks.
631        let flags = self.ctx.pack_relation_flags() & !RelationCacheKey::FLAG_STRICT_FUNCTION_TYPES;
632
633        if is_cacheable {
634            // Note: For assignability checks, we use AnyPropagationMode::All (0)
635            // since the checker doesn't track depth like SubtypeChecker does
636            let cache_key = RelationCacheKey::assignability(source, target, flags, 0);
637
638            if let Some(cached) = self.ctx.types.lookup_assignability_cache(cache_key) {
639                return cached;
640            }
641        }
642
643        let env = self.ctx.type_env.borrow();
644        // Preserve existing behavior: bivariant path does not use checker overrides.
645        let result = is_assignable_bivariant_with_resolver(
646            self.ctx.types,
647            &*env,
648            source,
649            target,
650            flags,
651            &self.ctx.inheritance_graph,
652            self.ctx.sound_mode(),
653        );
654
655        // Cache the result for non-inference types
656        // Use ORIGINAL types for cache key (not evaluated types)
657        if is_cacheable {
658            let cache_key = RelationCacheKey::assignability(source, target, flags, 0);
659
660            self.ctx.types.insert_assignability_cache(cache_key, result);
661        }
662
663        trace!(
664            source = source.0,
665            target = target.0,
666            result,
667            "is_assignable_to_bivariant"
668        );
669        result
670    }
671
672    /// Check if two types have any overlap (can ever be equal).
673    ///
674    /// Used for TS2367: "This condition will always return 'false'/'true' since
675    /// the types 'X' and 'Y' have no overlap."
676    ///
677    /// Returns true if the types can potentially be equal, false if they can never
678    /// have any common value.
679    pub fn are_types_overlapping(&mut self, left: TypeId, right: TypeId) -> bool {
680        // Ensure centralized relation preconditions before overlap check.
681        self.ensure_relation_input_ready(left);
682        self.ensure_relation_input_ready(right);
683
684        let env = self.ctx.type_env.borrow();
685        are_types_overlapping_with_env(
686            self.ctx.types,
687            &env,
688            left,
689            right,
690            self.ctx.strict_null_checks(),
691        )
692    }
693
694    // =========================================================================
695    // Weak Union and Excess Property Checking
696    // =========================================================================
697
698    /// Check if we should skip the general assignability error for an object literal.
699    /// Returns true if:
700    /// 1. It's a weak union violation (TypeScript shows excess property error instead)
701    /// 2. OR if the object literal has excess properties (TypeScript prioritizes TS2353 over TS2345/TS2322)
702    pub(crate) fn should_skip_weak_union_error(
703        &mut self,
704        source: TypeId,
705        target: TypeId,
706        source_idx: NodeIndex,
707    ) -> bool {
708        let Some(node) = self.ctx.arena.get(source_idx) else {
709            return false;
710        };
711        if node.kind != syntax_kind_ext::OBJECT_LITERAL_EXPRESSION {
712            return false;
713        }
714
715        // Check for weak union violation first (using scoped borrow)
716        if self.is_weak_union_violation(source, target) {
717            return true;
718        }
719
720        // Check if there are excess properties.
721        if !self.object_literal_has_excess_properties(source, target, source_idx) {
722            return false;
723        }
724
725        // There are excess properties. Check if all matching properties have compatible types.
726        let Some(source_shape) = object_shape_for_type(self.ctx.types, source) else {
727            return true;
728        };
729
730        let resolved_target = self.resolve_type_for_property_access(target);
731        let Some(target_shape) = object_shape_for_type(self.ctx.types, resolved_target) else {
732            return true;
733        };
734
735        let source_props = source_shape.properties.as_slice();
736        let target_props = target_shape.properties.as_slice();
737
738        // Check if any source property that exists in target has a wrong type
739        for source_prop in source_props {
740            if let Some(target_prop) = target_props.iter().find(|p| p.name == source_prop.name) {
741                let source_prop_type = source_prop.type_id;
742                let target_prop_type = target_prop.type_id;
743
744                let effective_target_type = if target_prop.optional {
745                    self.ctx
746                        .types
747                        .union(vec![target_prop_type, TypeId::UNDEFINED])
748                } else {
749                    target_prop_type
750                };
751
752                let is_assignable =
753                    { self.is_assignable_to(source_prop_type, effective_target_type) };
754
755                if !is_assignable {
756                    return false;
757                }
758            }
759        }
760
761        true
762    }
763
764    /// Check assignability and emit the standard TS2322/TS2345-style diagnostic when needed.
765    pub(crate) fn check_satisfies_assignable_or_report(
766        &mut self,
767        source: TypeId,
768        target: TypeId,
769        source_idx: NodeIndex,
770    ) -> bool {
771        let diag_idx = source_idx;
772        let source = self.narrow_this_from_enclosing_typeof_guard(source_idx, source);
773        if self.should_suppress_assignability_diagnostic(source, target) {
774            return true;
775        }
776        if self.should_suppress_assignability_for_parse_recovery(source_idx, diag_idx) {
777            return true;
778        }
779
780        if tsz_solver::type_queries::is_keyof_type(self.ctx.types, target)
781            && let Some(str_lit) =
782                tsz_solver::type_queries::get_string_literal_value(self.ctx.types, source)
783        {
784            let keyof_type =
785                tsz_solver::type_queries::get_keyof_type(self.ctx.types, target).unwrap();
786            let allowed_keys = self.get_keyof_type_keys(keyof_type, self.ctx.types);
787            if !allowed_keys.contains(&str_lit) {
788                self.error_type_does_not_satisfy_the_expected_type(source, target, diag_idx);
789                return false;
790            }
791        }
792
793        if let Some(node) = self.ctx.arena.get(source_idx)
794            && node.kind == syntax_kind_ext::OBJECT_LITERAL_EXPRESSION
795        {
796            self.check_object_literal_excess_properties(source, target, source_idx);
797        }
798
799        if self.is_assignable_to(source, target)
800            || self.should_skip_weak_union_error(source, target, source_idx)
801        {
802            return true;
803        }
804        self.error_type_does_not_satisfy_the_expected_type(source, target, diag_idx);
805        false
806    }
807
808    ///
809    /// Returns true when no diagnostic was emitted (assignable or intentionally skipped),
810    /// false when an assignability diagnostic was emitted.
811    pub(crate) fn check_assignable_or_report(
812        &mut self,
813        source: TypeId,
814        target: TypeId,
815        source_idx: NodeIndex,
816    ) -> bool {
817        self.check_assignable_or_report_at(source, target, source_idx, source_idx)
818    }
819
820    /// Check assignability and emit TS2322/TS2345-style diagnostics with independent
821    /// source and diagnostic anchors.
822    ///
823    /// `source_idx` is used for weak-union/excess-property prioritization.
824    /// `diag_idx` is where the assignability diagnostic is anchored.
825    pub(crate) fn check_assignable_or_report_at(
826        &mut self,
827        source: TypeId,
828        target: TypeId,
829        source_idx: NodeIndex,
830        diag_idx: NodeIndex,
831    ) -> bool {
832        let source = self.narrow_this_from_enclosing_typeof_guard(source_idx, source);
833        if self.should_suppress_assignability_diagnostic(source, target) {
834            return true;
835        }
836        if self.should_suppress_assignability_for_parse_recovery(source_idx, diag_idx) {
837            return true;
838        }
839
840        if tsz_solver::type_queries::is_keyof_type(self.ctx.types, target)
841            && let Some(str_lit) =
842                tsz_solver::type_queries::get_string_literal_value(self.ctx.types, source)
843        {
844            let keyof_type =
845                tsz_solver::type_queries::get_keyof_type(self.ctx.types, target).unwrap();
846            let allowed_keys = self.get_keyof_type_keys(keyof_type, self.ctx.types);
847            if !allowed_keys.contains(&str_lit) {
848                self.error_type_not_assignable_with_reason_at(source, target, diag_idx);
849                return false;
850            }
851        }
852
853        if self.is_assignable_to(source, target)
854            || self.should_skip_weak_union_error(source, target, source_idx)
855        {
856            return true;
857        }
858        self.error_type_not_assignable_with_reason_at(source, target, diag_idx);
859        false
860    }
861
862    /// Check assignability and emit a generic TS2322 diagnostic at `diag_idx`.
863    ///
864    /// This is used for call sites that intentionally avoid detailed reason rendering
865    /// but still share centralized mismatch/suppression behavior.
866    pub(crate) fn check_assignable_or_report_generic_at(
867        &mut self,
868        source: TypeId,
869        target: TypeId,
870        source_idx: NodeIndex,
871        diag_idx: NodeIndex,
872    ) -> bool {
873        let source = self.narrow_this_from_enclosing_typeof_guard(source_idx, source);
874        if self.should_suppress_assignability_diagnostic(source, target) {
875            return true;
876        }
877        if self.should_suppress_assignability_for_parse_recovery(source_idx, diag_idx) {
878            return true;
879        }
880        if self.is_assignable_to(source, target)
881            || self.should_skip_weak_union_error(source, target, source_idx)
882        {
883            return true;
884        }
885        self.error_type_not_assignable_generic_at(source, target, diag_idx);
886        false
887    }
888
889    /// Check assignability and emit argument-not-assignable diagnostics (TS2345-style).
890    ///
891    /// Returns true when no diagnostic was emitted (assignable or intentionally skipped),
892    /// false when an argument-assignability diagnostic was emitted.
893    pub(crate) fn check_argument_assignable_or_report(
894        &mut self,
895        source: TypeId,
896        target: TypeId,
897        arg_idx: NodeIndex,
898    ) -> bool {
899        if self.should_suppress_assignability_diagnostic(source, target) {
900            return true;
901        }
902        if self.should_suppress_assignability_for_parse_recovery(arg_idx, arg_idx) {
903            return true;
904        }
905        if self.is_assignable_to(source, target) {
906            return true;
907        }
908        if self.should_skip_weak_union_error(source, target, arg_idx) {
909            return true;
910        }
911        self.error_argument_not_assignable_at(source, target, arg_idx);
912        false
913    }
914
915    /// Returns true when an assignability mismatch should produce a diagnostic.
916    ///
917    /// This centralizes the standard "not assignable + not weak-union/excess-property
918    /// suppression" decision so call sites emitting different diagnostics can share it.
919    pub(crate) fn should_report_assignability_mismatch(
920        &mut self,
921        source: TypeId,
922        target: TypeId,
923        source_idx: NodeIndex,
924    ) -> bool {
925        if self.should_suppress_assignability_diagnostic(source, target) {
926            return false;
927        }
928        if self.should_suppress_assignability_for_parse_recovery(source_idx, source_idx) {
929            return false;
930        }
931        !self.is_assignable_to(source, target)
932            && !self.should_skip_weak_union_error(source, target, source_idx)
933    }
934
935    /// Returns true when a bivariant-assignability mismatch should produce a diagnostic.
936    ///
937    /// Mirrors `should_report_assignability_mismatch` but uses the bivariant relation
938    /// entrypoint for method-compatibility scenarios.
939    pub(crate) fn should_report_assignability_mismatch_bivariant(
940        &mut self,
941        source: TypeId,
942        target: TypeId,
943        source_idx: NodeIndex,
944    ) -> bool {
945        if self.should_suppress_assignability_diagnostic(source, target) {
946            return false;
947        }
948        if self.should_suppress_assignability_for_parse_recovery(source_idx, source_idx) {
949            return false;
950        }
951        !self.is_assignable_to_bivariant(source, target)
952            && !self.should_skip_weak_union_error(source, target, source_idx)
953    }
954
955    /// Check bidirectional assignability.
956    ///
957    /// Useful in checker locations that need type comparability/equivalence-like checks.
958    pub(crate) fn are_mutually_assignable(&mut self, left: TypeId, right: TypeId) -> bool {
959        self.is_assignable_to(left, right) && self.is_assignable_to(right, left)
960    }
961
962    /// Check if two types are comparable (overlap).
963    ///
964    /// Corresponds to TypeScript's `isTypeComparableTo`: returns true if the types
965    /// have any overlap. TSC's comparableRelation differs from assignability:
966    /// - For union sources: uses `someTypeRelatedToType` (ANY member suffices)
967    /// - For union targets: also checks per-member overlap
968    ///
969    /// Used for switch/case comparability (TS2678), equality narrowing, etc.
970    pub(crate) fn is_type_comparable_to(&mut self, source: TypeId, target: TypeId) -> bool {
971        // Fast path: direct bidirectional assignability
972        if self.is_assignable_to(source, target) || self.is_assignable_to(target, source) {
973            return true;
974        }
975
976        // TSC's comparable relation decomposes unions and checks if ANY member
977        // is related to the other type. This handles cases like:
978        // - `User.A | User.B` comparable to `User.A` (User.A member matches)
979        // - `string & Brand` comparable to `"a"` (string member of intersection)
980        use crate::query_boundaries::dispatch as query;
981
982        // Decompose source union: check if any member is assignable in either direction
983        if let Some(members) = query::union_members(self.ctx.types, source) {
984            for member in &members {
985                if self.is_assignable_to(*member, target) || self.is_assignable_to(target, *member)
986                {
987                    return true;
988                }
989            }
990        }
991
992        // Decompose target union: check if any member is assignable in either direction
993        if let Some(members) = query::union_members(self.ctx.types, target) {
994            for member in &members {
995                if self.is_assignable_to(source, *member) || self.is_assignable_to(*member, source)
996                {
997                    return true;
998                }
999            }
1000        }
1001
1002        false
1003    }
1004
1005    /// Check if source object literal has properties that don't exist in target.
1006    ///
1007    /// Uses TypeId-based freshness tracking (fresh object literals only).
1008    pub(crate) fn object_literal_has_excess_properties(
1009        &mut self,
1010        source: TypeId,
1011        target: TypeId,
1012        _source_idx: NodeIndex,
1013    ) -> bool {
1014        use tsz_solver::relations::freshness;
1015        // Only fresh object literals trigger excess property checking.
1016        if !freshness::is_fresh_object_type(self.ctx.types, source) {
1017            return false;
1018        }
1019
1020        let Some(source_shape) = object_shape_for_type(self.ctx.types, source) else {
1021            return false;
1022        };
1023
1024        let source_props = source_shape.properties.as_slice();
1025        if source_props.is_empty() {
1026            return false;
1027        }
1028
1029        let resolved_target = self.resolve_type_for_property_access(target);
1030
1031        match classify_for_excess_properties(self.ctx.types, resolved_target) {
1032            ExcessPropertiesKind::Object(shape_id) => {
1033                let target_shape = self.ctx.types.object_shape(shape_id);
1034                let target_props = target_shape.properties.as_slice();
1035
1036                if target_props.is_empty() {
1037                    return false;
1038                }
1039
1040                if target_shape.string_index.is_some() || target_shape.number_index.is_some() {
1041                    return false;
1042                }
1043
1044                source_props
1045                    .iter()
1046                    .any(|source_prop| !target_props.iter().any(|p| p.name == source_prop.name))
1047            }
1048            ExcessPropertiesKind::ObjectWithIndex(_shape_id) => false,
1049            ExcessPropertiesKind::Union(members) => {
1050                let mut target_shapes = Vec::new();
1051                let mut matched_shapes = Vec::new();
1052
1053                for member in members {
1054                    let resolved_member = self.resolve_type_for_property_access(member);
1055                    let Some(shape) = object_shape_for_type(self.ctx.types, resolved_member) else {
1056                        // If a union member has no object shape and is a type parameter
1057                        // or the `object` intrinsic, it accepts any properties, so EPC
1058                        // should not apply.
1059                        if tsz_solver::type_queries::is_type_parameter_like(
1060                            self.ctx.types,
1061                            resolved_member,
1062                        ) || resolved_member == TypeId::OBJECT
1063                        {
1064                            return false;
1065                        }
1066                        continue;
1067                    };
1068
1069                    if shape.properties.is_empty()
1070                        || shape.string_index.is_some()
1071                        || shape.number_index.is_some()
1072                    {
1073                        return false;
1074                    }
1075
1076                    target_shapes.push(shape.clone());
1077
1078                    if self.is_subtype_of(source, resolved_member) {
1079                        matched_shapes.push(shape);
1080                    }
1081                }
1082
1083                if target_shapes.is_empty() {
1084                    return false;
1085                }
1086
1087                let effective_shapes = if matched_shapes.is_empty() {
1088                    target_shapes
1089                } else {
1090                    matched_shapes
1091                };
1092
1093                source_props.iter().any(|source_prop| {
1094                    !effective_shapes.iter().any(|shape| {
1095                        shape
1096                            .properties
1097                            .iter()
1098                            .any(|prop| prop.name == source_prop.name)
1099                    })
1100                })
1101            }
1102            ExcessPropertiesKind::Intersection(members) => {
1103                let mut target_shapes = Vec::new();
1104
1105                for member in members {
1106                    let resolved_member = self.resolve_type_for_property_access(member);
1107                    let Some(shape) = object_shape_for_type(self.ctx.types, resolved_member) else {
1108                        continue;
1109                    };
1110
1111                    if shape.string_index.is_some() || shape.number_index.is_some() {
1112                        return false;
1113                    }
1114
1115                    target_shapes.push(shape);
1116                }
1117
1118                if target_shapes.is_empty() {
1119                    return false;
1120                }
1121
1122                source_props.iter().any(|source_prop| {
1123                    !target_shapes.iter().any(|shape| {
1124                        shape
1125                            .properties
1126                            .iter()
1127                            .any(|prop| prop.name == source_prop.name)
1128                    })
1129                })
1130            }
1131            ExcessPropertiesKind::NotObject => false,
1132        }
1133    }
1134
1135    pub(crate) fn analyze_assignability_failure(
1136        &mut self,
1137        source: TypeId,
1138        target: TypeId,
1139    ) -> crate::query_boundaries::assignability::AssignabilityFailureAnalysis {
1140        // Keep failure analysis on the same relation boundary as `is_assignable_to`
1141        // (CheckerContext resolver + checker overrides) so mismatch suppression and
1142        // diagnostic rendering observe identical compatibility semantics.
1143        let overrides = CheckerOverrideProvider::new(self, None);
1144        let inputs = AssignabilityQueryInputs {
1145            db: self.ctx.types,
1146            resolver: &self.ctx,
1147            source,
1148            target,
1149            flags: self.ctx.pack_relation_flags(),
1150            inheritance_graph: &self.ctx.inheritance_graph,
1151            sound_mode: self.ctx.sound_mode(),
1152        };
1153        let gate = check_assignable_gate_with_overrides(&inputs, &overrides, Some(&self.ctx), true);
1154        if gate.related {
1155            return crate::query_boundaries::assignability::AssignabilityFailureAnalysis {
1156                weak_union_violation: false,
1157                failure_reason: None,
1158            };
1159        }
1160        gate.analysis.unwrap_or(
1161            crate::query_boundaries::assignability::AssignabilityFailureAnalysis {
1162                weak_union_violation: false,
1163                failure_reason: None,
1164            },
1165        )
1166    }
1167
1168    pub(crate) fn is_weak_union_violation(&mut self, source: TypeId, target: TypeId) -> bool {
1169        self.analyze_assignability_failure(source, target)
1170            .weak_union_violation
1171    }
1172}