Skip to main content

tsz_checker/error_reporter/
assignability.rs

1//! Type assignability error reporting (TS2322 and related).
2
3use crate::diagnostics::{
4    Diagnostic, DiagnosticCategory, DiagnosticRelatedInformation, diagnostic_codes,
5    diagnostic_messages, format_message,
6};
7use crate::state::{CheckerState, MemberAccessLevel};
8use tracing::{Level, trace};
9use tsz_parser::parser::NodeIndex;
10use tsz_solver::TypeId;
11
12impl<'a> CheckerState<'a> {
13    // =========================================================================
14    // Type Assignability Errors
15    // =========================================================================
16
17    /// Report a type not assignable error (delegates to `diagnose_assignment_failure`).
18    pub fn error_type_not_assignable_at(&mut self, source: TypeId, target: TypeId, idx: NodeIndex) {
19        let anchor_idx = self.assignment_diagnostic_anchor_idx(idx);
20        self.diagnose_assignment_failure_with_anchor(source, target, anchor_idx);
21    }
22
23    /// Report a type not assignable error at an exact AST node anchor.
24    pub fn error_type_not_assignable_at_with_anchor(
25        &mut self,
26        source: TypeId,
27        target: TypeId,
28        anchor_idx: NodeIndex,
29    ) {
30        self.diagnose_assignment_failure_with_anchor(source, target, anchor_idx);
31    }
32    pub fn error_type_does_not_satisfy_the_expected_type(
33        &mut self,
34        source: TypeId,
35        target: TypeId,
36        idx: NodeIndex,
37    ) {
38        let anchor_idx = self.assignment_diagnostic_anchor_idx(idx);
39
40        if self.should_suppress_assignability_diagnostic(source, target) {
41            return;
42        }
43
44        let reason = self
45            .analyze_assignability_failure(source, target)
46            .failure_reason;
47
48        let mut base_diag = match reason {
49            Some(reason) => self.render_failure_reason(&reason, source, target, anchor_idx, 0),
50            None => {
51                let Some(loc) = self.get_source_location(anchor_idx) else {
52                    return;
53                };
54                let mut builder = tsz_solver::SpannedDiagnosticBuilder::with_symbols(
55                    self.ctx.types,
56                    &self.ctx.binder.symbols,
57                    self.ctx.file_name.as_str(),
58                )
59                .with_def_store(&self.ctx.definition_store);
60                let diag = builder.type_not_assignable(source, target, loc.start, loc.length());
61                diag.to_checker_diagnostic(&self.ctx.file_name)
62            }
63        };
64
65        // Mutate the top-level diagnostic to be TS1360
66        let src_str = self.format_type_for_assignability_message(source);
67        let tgt_str = self.format_type_for_assignability_message(target);
68        use tsz_common::diagnostics::data::diagnostic_codes;
69        use tsz_common::diagnostics::data::diagnostic_messages;
70        use tsz_common::diagnostics::format_message;
71
72        let msg = format_message(
73            diagnostic_messages::TYPE_DOES_NOT_SATISFY_THE_EXPECTED_TYPE,
74            &[&src_str, &tgt_str],
75        );
76
77        if base_diag.code != diagnostic_codes::TYPE_DOES_NOT_SATISFY_THE_EXPECTED_TYPE {
78            let mut new_related = vec![];
79
80            new_related.push(tsz_common::diagnostics::DiagnosticRelatedInformation {
81                category: tsz_common::diagnostics::DiagnosticCategory::Error,
82                code: base_diag.code,
83                file: base_diag.file.clone(),
84                start: base_diag.start,
85                length: base_diag.length,
86                message_text: base_diag.message_text.clone(),
87            });
88
89            new_related.extend(base_diag.related_information);
90
91            base_diag.code = diagnostic_codes::TYPE_DOES_NOT_SATISFY_THE_EXPECTED_TYPE;
92            base_diag.message_text = msg;
93            base_diag.related_information = new_related;
94        }
95
96        self.ctx.diagnostics.push(base_diag);
97    }
98
99    /// Diagnose why an assignment failed and report a detailed error.
100    pub fn diagnose_assignment_failure(&mut self, source: TypeId, target: TypeId, idx: NodeIndex) {
101        let anchor_idx = self.assignment_diagnostic_anchor_idx(idx);
102        self.diagnose_assignment_failure_with_anchor(source, target, anchor_idx);
103    }
104
105    /// Internal helper that reports a detailed assignability failure using an
106    /// already-resolved diagnostic anchor.
107    fn diagnose_assignment_failure_with_anchor(
108        &mut self,
109        source: TypeId,
110        target: TypeId,
111        anchor_idx: NodeIndex,
112    ) {
113        // Centralized suppression for TS2322 cascades on unresolved escape-hatch types.
114        if self.should_suppress_assignability_diagnostic(source, target) {
115            if tracing::enabled!(Level::TRACE) {
116                trace!(
117                    source = source.0,
118                    target = target.0,
119                    node_idx = anchor_idx.0,
120                    file = %self.ctx.file_name,
121                    "suppressing TS2322 for non-actionable source/target types"
122                );
123            }
124            return;
125        }
126
127        // Check for constructor accessibility mismatch
128        if let Some((source_level, target_level)) =
129            self.constructor_accessibility_mismatch(source, target, None)
130        {
131            self.error_constructor_accessibility_not_assignable(
132                source,
133                target,
134                source_level,
135                target_level,
136                anchor_idx,
137            );
138            return;
139        }
140
141        // Check for private brand mismatch
142        if let Some(detail) = self.private_brand_mismatch_error(source, target) {
143            let Some(loc) = self.get_node_span(anchor_idx) else {
144                return;
145            };
146
147            let source_type = self.format_type(source);
148            let target_type = self.format_type(target);
149            let message = format_message(
150                diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
151                &[&source_type, &target_type],
152            );
153
154            let diag = Diagnostic::error(
155                self.ctx.file_name.clone(),
156                loc.0,
157                loc.1 - loc.0,
158                message,
159                diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
160            )
161            .with_related(self.ctx.file_name.clone(), loc.0, loc.1 - loc.0, detail);
162
163            self.ctx.diagnostics.push(diag);
164            return;
165        }
166
167        // Use one solver-boundary analysis path for TS2322 metadata.
168        let analysis = self.analyze_assignability_failure(source, target);
169        let reason = analysis.failure_reason;
170
171        if tracing::enabled!(Level::TRACE) {
172            let source_type = self.format_type(source);
173            let target_type = self.format_type(target);
174            let reason_ref = reason.as_ref();
175            trace!(
176                source = %source_type,
177                target = %target_type,
178                reason = ?reason_ref,
179                node_idx = anchor_idx.0,
180                file = %self.ctx.file_name,
181                "assignability failure diagnostics"
182            );
183        }
184
185        match reason {
186            Some(failure_reason) => {
187                let diag =
188                    self.render_failure_reason(&failure_reason, source, target, anchor_idx, 0);
189                self.ctx.diagnostics.push(diag);
190            }
191            None => {
192                // Fallback to generic message
193                self.error_type_not_assignable_generic_at(source, target, anchor_idx);
194            }
195        }
196    }
197
198    /// Internal generic error reporting for type assignability failures.
199    pub(crate) fn error_type_not_assignable_generic_at(
200        &mut self,
201        source: TypeId,
202        target: TypeId,
203        idx: NodeIndex,
204    ) {
205        let anchor_idx = self.assignment_diagnostic_anchor_idx(idx);
206
207        // Suppress cascade errors from unresolved types
208        if source == TypeId::ERROR
209            || target == TypeId::ERROR
210            || source == TypeId::ANY
211            || target == TypeId::ANY
212            || source == TypeId::UNKNOWN
213            || target == TypeId::UNKNOWN
214        {
215            return;
216        }
217
218        if let Some(loc) = self.get_source_location(anchor_idx) {
219            // Precedence gate: suppress fallback TS2322 when a more specific
220            // diagnostic is already present at the same span.
221            if self.has_more_specific_diagnostic_at_span(loc.start, loc.length()) {
222                return;
223            }
224
225            let mut builder = tsz_solver::SpannedDiagnosticBuilder::with_symbols(
226                self.ctx.types,
227                &self.ctx.binder.symbols,
228                self.ctx.file_name.as_str(),
229            )
230            .with_def_store(&self.ctx.definition_store);
231            let diag = builder.type_not_assignable(source, target, loc.start, loc.length());
232            self.ctx
233                .diagnostics
234                .push(diag.to_checker_diagnostic(&self.ctx.file_name));
235        }
236    }
237
238    /// Recursively render a `SubtypeFailureReason` into a Diagnostic.
239    fn render_failure_reason(
240        &mut self,
241        reason: &tsz_solver::SubtypeFailureReason,
242        source: TypeId,
243        target: TypeId,
244        idx: NodeIndex,
245        depth: u32,
246    ) -> Diagnostic {
247        use tsz_solver::SubtypeFailureReason;
248
249        let (start, length) = self.get_node_span(idx).unwrap_or((0, 0));
250        let file_name = self.ctx.file_name.clone();
251
252        match reason {
253            SubtypeFailureReason::MissingProperty {
254                property_name,
255                source_type,
256                target_type,
257            } => {
258                // TSC emits TS2322 (generic assignability error) instead of TS2741
259                // when the source is a primitive type. Primitives can't have "missing properties".
260                // Example: `x: number = moduleA` → "Type '...' is not assignable to type 'number'"
261                //          NOT "Property 'someClass' is missing in type 'number'..."
262                if tsz_solver::is_primitive_type(self.ctx.types, *source_type) {
263                    let src_str = self.format_type(*source_type);
264                    let tgt_str = self.format_type(*target_type);
265                    let message = format_message(
266                        diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
267                        &[&src_str, &tgt_str],
268                    );
269                    return Diagnostic::error(
270                        file_name,
271                        start,
272                        length,
273                        message,
274                        diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
275                    );
276                }
277
278                // Also emit TS2322 for wrapper-like built-ins (Boolean, Number, String, Object)
279                // instead of TS2741.
280                // These built-in types inherit properties from Object, and object literals don't
281                // explicitly list inherited properties, so TS2741 would be incorrect.
282                // Example: `b: Boolean = {}` → TS2322 "Type '{}' is not assignable to type 'Boolean'"
283                //          NOT TS2741 "Property 'valueOf' is missing in type '{}'..."
284                let tgt_str = self.format_type(*target_type);
285                if tgt_str == "Boolean"
286                    || tgt_str == "Number"
287                    || tgt_str == "String"
288                    || tgt_str == "Object"
289                {
290                    let src_str = self.format_type(*source_type);
291                    let message = format_message(
292                        diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
293                        &[&src_str, &tgt_str],
294                    );
295                    return Diagnostic::error(
296                        file_name,
297                        start,
298                        length,
299                        message,
300                        diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
301                    );
302                }
303
304                // Private brand properties are internal implementation details for
305                // nominal private member checking. They should never appear in
306                // user-facing diagnostics — emit TS2322 instead of TS2741.
307                let prop_name = self.ctx.types.resolve_atom_ref(*property_name);
308                if prop_name.starts_with("__private_brand") {
309                    let src_str = self.format_type(*source_type);
310                    let message = format_message(
311                        diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
312                        &[&src_str, &tgt_str],
313                    );
314                    return Diagnostic::error(
315                        file_name,
316                        start,
317                        length,
318                        message,
319                        diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
320                    );
321                }
322
323                // TS2741: Property 'x' is missing in type 'A' but required in type 'B'.
324                let src_str = self.format_type(*source_type);
325                let message = format_message(
326                    diagnostic_messages::PROPERTY_IS_MISSING_IN_TYPE_BUT_REQUIRED_IN_TYPE,
327                    &[&prop_name, &src_str, &tgt_str],
328                );
329                Diagnostic::error(
330                    file_name,
331                    start,
332                    length,
333                    message,
334                    diagnostic_codes::PROPERTY_IS_MISSING_IN_TYPE_BUT_REQUIRED_IN_TYPE,
335                )
336            }
337
338            SubtypeFailureReason::MissingProperties {
339                property_names,
340                source_type,
341                target_type,
342            } => {
343                // TSC emits TS2322 (generic assignability error) instead of TS2739/TS2740
344                // when the source is a primitive type. Primitives can't have "missing properties".
345                // Example: `arguments = 10` where arguments is IArguments
346                //          → "Type 'number' is not assignable to type '...'"
347                //          NOT "Type 'number' is missing properties from type '...'"
348                if tsz_solver::is_primitive_type(self.ctx.types, *source_type) {
349                    let src_str = self.format_type(*source_type);
350                    let tgt_str = self.format_type(*target_type);
351                    let message = format_message(
352                        diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
353                        &[&src_str, &tgt_str],
354                    );
355                    return Diagnostic::error(
356                        file_name,
357                        start,
358                        length,
359                        message,
360                        diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
361                    );
362                }
363
364                // Also emit TS2322 for wrapper-like built-ins (Boolean, Number, String, Object)
365                // instead of TS2739/TS2740.
366                // These built-in types inherit properties from Object, and object literals don't
367                // explicitly list inherited properties, so TS2739 would be incorrect.
368                let tgt_str_check = self.format_type(*target_type);
369                if tgt_str_check == "Boolean"
370                    || tgt_str_check == "Number"
371                    || tgt_str_check == "String"
372                    || tgt_str_check == "Object"
373                {
374                    let src_str = self.format_type(*source_type);
375                    let message = format_message(
376                        diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
377                        &[&src_str, &tgt_str_check],
378                    );
379                    return Diagnostic::error(
380                        file_name,
381                        start,
382                        length,
383                        message,
384                        diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
385                    );
386                }
387
388                // Filter out private brand properties — they are internal implementation
389                // details and should never appear in user-facing diagnostics.
390                let filtered_names: Vec<_> = property_names
391                    .iter()
392                    .filter(|name| {
393                        !self
394                            .ctx
395                            .types
396                            .resolve_atom_ref(**name)
397                            .starts_with("__private_brand")
398                    })
399                    .collect();
400
401                // If all missing properties are numeric indices, emit TS2322.
402                // TSC often emits TS2322 instead of TS2739 when assigning arrays/tuples to tuple-like interfaces.
403                let all_numeric = !filtered_names.is_empty()
404                    && filtered_names.iter().all(|name| {
405                        let s = self.ctx.types.resolve_atom_ref(**name);
406                        s.parse::<usize>().is_ok()
407                    });
408
409                if all_numeric {
410                    let src_str = self.format_type(*source_type);
411                    let tgt_str = self.format_type(*target_type);
412                    let message = format_message(
413                        diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
414                        &[&src_str, &tgt_str],
415                    );
416                    return Diagnostic::error(
417                        file_name,
418                        start,
419                        length,
420                        message,
421                        diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
422                    );
423                }
424
425                // If all missing properties were private brands, emit TS2322 instead.
426                if filtered_names.is_empty() {
427                    let src_str = self.format_type(*source_type);
428                    let tgt_str = self.format_type(*target_type);
429                    let message = format_message(
430                        diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
431                        &[&src_str, &tgt_str],
432                    );
433                    return Diagnostic::error(
434                        file_name,
435                        start,
436                        length,
437                        message,
438                        diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
439                    );
440                }
441
442                // TS2739: Type 'A' is missing the following properties from type 'B': x, y, z
443                // TS2740: Type 'A' is missing the following properties from type 'B': x, y, z, and N more.
444                let src_str = self.format_type(*source_type);
445                let tgt_str = self.format_type(*target_type);
446                let prop_list: Vec<String> = filtered_names
447                    .iter()
448                    .take(5)
449                    .map(|name| self.ctx.types.resolve_atom_ref(**name).to_string())
450                    .collect();
451                let props_joined = prop_list.join(", ");
452                // Use TS2740 when there are 5+ missing properties (tsc behavior)
453                if filtered_names.len() > 5 {
454                    let more_count = (filtered_names.len() - 5).to_string();
455                    let message = format_message(
456                        diagnostic_messages::TYPE_IS_MISSING_THE_FOLLOWING_PROPERTIES_FROM_TYPE_AND_MORE,
457                        &[&src_str, &tgt_str, &props_joined, &more_count],
458                    );
459                    Diagnostic::error(
460                        file_name,
461                        start,
462                        length,
463                        message,
464                        diagnostic_codes::TYPE_IS_MISSING_THE_FOLLOWING_PROPERTIES_FROM_TYPE_AND_MORE,
465                    )
466                } else {
467                    let message = format_message(
468                        diagnostic_messages::TYPE_IS_MISSING_THE_FOLLOWING_PROPERTIES_FROM_TYPE,
469                        &[&src_str, &tgt_str, &props_joined],
470                    );
471                    Diagnostic::error(
472                        file_name,
473                        start,
474                        length,
475                        message,
476                        diagnostic_codes::TYPE_IS_MISSING_THE_FOLLOWING_PROPERTIES_FROM_TYPE,
477                    )
478                }
479            }
480
481            SubtypeFailureReason::PropertyTypeMismatch {
482                property_name,
483                source_property_type,
484                target_property_type,
485                nested_reason,
486            } => {
487                if depth == 0 {
488                    let source_str = self.format_type_for_assignability_message(source);
489                    let target_str = self.format_type_for_assignability_message(target);
490                    let base = format_message(
491                        diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
492                        &[&source_str, &target_str],
493                    );
494                    // TODO: tsc emits property type mismatch elaboration as related information
495                    return Diagnostic::error(
496                        file_name,
497                        start,
498                        length,
499                        base,
500                        diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
501                    );
502                }
503
504                let prop_name = self.ctx.types.resolve_atom_ref(*property_name);
505                let message = format_message(
506                    diagnostic_messages::TYPES_OF_PROPERTY_ARE_INCOMPATIBLE,
507                    &[&prop_name],
508                );
509                let mut diag =
510                    Diagnostic::error(file_name, start, length, message, reason.diagnostic_code());
511
512                if let Some(nested) = nested_reason
513                    && depth < 5
514                {
515                    let nested_diag = self.render_failure_reason(
516                        nested,
517                        *source_property_type,
518                        *target_property_type,
519                        idx,
520                        depth + 1,
521                    );
522                    diag.related_information.push(DiagnosticRelatedInformation {
523                        file: nested_diag.file,
524                        start: nested_diag.start,
525                        length: nested_diag.length,
526                        message_text: nested_diag.message_text,
527                        category: DiagnosticCategory::Message,
528                        code: nested_diag.code,
529                    });
530                }
531                diag
532            }
533
534            SubtypeFailureReason::OptionalPropertyRequired { property_name } => {
535                // At depth 0, emit TS2322 as the primary error (matching tsc behavior).
536                if depth == 0 {
537                    let source_str = self.format_type_for_assignability_message(source);
538                    let target_str = self.format_type_for_assignability_message(target);
539                    let base = format_message(
540                        diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
541                        &[&source_str, &target_str],
542                    );
543                    // TODO: tsc emits property optional/required elaboration as related information
544                    Diagnostic::error(
545                        file_name,
546                        start,
547                        length,
548                        base,
549                        diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
550                    )
551                } else {
552                    let prop_name = self.ctx.types.resolve_atom_ref(*property_name);
553                    let source_str = self.format_type(source);
554                    let target_str = self.format_type(target);
555                    let message = format_message(
556                        diagnostic_messages::PROPERTY_IS_MISSING_IN_TYPE_BUT_REQUIRED_IN_TYPE,
557                        &[&prop_name, &source_str, &target_str],
558                    );
559                    Diagnostic::error(file_name, start, length, message, reason.diagnostic_code())
560                }
561            }
562
563            SubtypeFailureReason::ReadonlyPropertyMismatch { property_name } => {
564                let prop_name = self.ctx.types.resolve_atom_ref(*property_name);
565                let message = format_message(
566                    diagnostic_messages::CANNOT_ASSIGN_TO_BECAUSE_IT_IS_A_READ_ONLY_PROPERTY,
567                    &[&prop_name],
568                );
569                Diagnostic::error(file_name, start, length, message, reason.diagnostic_code())
570            }
571
572            SubtypeFailureReason::PropertyVisibilityMismatch { .. } => {
573                let source_str = self.format_type_for_assignability_message(source);
574                let target_str = self.format_type_for_assignability_message(target);
575                let base = format_message(
576                    diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
577                    &[&source_str, &target_str],
578                );
579                // TODO: tsc emits visibility elaboration as related information
580                Diagnostic::error(
581                    file_name,
582                    start,
583                    length,
584                    base,
585                    diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
586                )
587            }
588
589            SubtypeFailureReason::PropertyNominalMismatch { .. } => {
590                let source_str = self.format_type_for_assignability_message(source);
591                let target_str = self.format_type_for_assignability_message(target);
592                let base = format_message(
593                    diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
594                    &[&source_str, &target_str],
595                );
596                // TODO: tsc emits nominal mismatch elaboration as related information
597                Diagnostic::error(
598                    file_name,
599                    start,
600                    length,
601                    base,
602                    diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
603                )
604            }
605
606            SubtypeFailureReason::ExcessProperty {
607                property_name,
608                target_type,
609            } => {
610                let prop_name = self.ctx.types.resolve_atom_ref(*property_name);
611                let target_str = self.format_type(*target_type);
612                let message = format_message(
613                    diagnostic_messages::OBJECT_LITERAL_MAY_ONLY_SPECIFY_KNOWN_PROPERTIES_AND_DOES_NOT_EXIST_IN_TYPE,
614                    &[&prop_name, &target_str],
615                );
616                Diagnostic::error(file_name, start, length, message, reason.diagnostic_code())
617            }
618
619            SubtypeFailureReason::ReturnTypeMismatch {
620                source_return,
621                target_return,
622                nested_reason,
623            } => {
624                let source_str = self.format_type(*source_return);
625                let target_str = self.format_type(*target_return);
626                let message =
627                    format!("Return type '{source_str}' is not assignable to '{target_str}'.");
628                let mut diag =
629                    Diagnostic::error(file_name, start, length, message, reason.diagnostic_code());
630
631                if let Some(nested) = nested_reason
632                    && depth < 5
633                {
634                    let nested_diag = self.render_failure_reason(
635                        nested,
636                        *source_return,
637                        *target_return,
638                        idx,
639                        depth + 1,
640                    );
641                    diag.related_information.push(DiagnosticRelatedInformation {
642                        file: nested_diag.file,
643                        start: nested_diag.start,
644                        length: nested_diag.length,
645                        message_text: nested_diag.message_text,
646                        category: DiagnosticCategory::Message,
647                        code: nested_diag.code,
648                    });
649                }
650                diag
651            }
652
653            SubtypeFailureReason::TooManyParameters {
654                source_count,
655                target_count,
656            } => {
657                let message = format_message(
658                    diagnostic_messages::EXPECTED_ARGUMENTS_BUT_GOT,
659                    &[&target_count.to_string(), &source_count.to_string()],
660                );
661                Diagnostic::error(file_name, start, length, message, reason.diagnostic_code())
662            }
663
664            SubtypeFailureReason::TupleElementMismatch {
665                source_count,
666                target_count,
667            } => {
668                let message = format!(
669                    "Tuple type has {source_count} elements but target requires {target_count}."
670                );
671                Diagnostic::error(file_name, start, length, message, reason.diagnostic_code())
672            }
673
674            SubtypeFailureReason::TupleElementTypeMismatch {
675                index,
676                source_element,
677                target_element,
678            } => {
679                let source_str = self.format_type(*source_element);
680                let target_str = self.format_type(*target_element);
681                let message = format!(
682                    "Type of element at index {index} is incompatible: '{source_str}' is not assignable to '{target_str}'."
683                );
684                Diagnostic::error(file_name, start, length, message, reason.diagnostic_code())
685            }
686
687            SubtypeFailureReason::ArrayElementMismatch {
688                source_element,
689                target_element,
690            } => {
691                let source_str = self.format_type(*source_element);
692                let target_str = self.format_type(*target_element);
693                let message = format!(
694                    "Array element type '{source_str}' is not assignable to '{target_str}'."
695                );
696                Diagnostic::error(file_name, start, length, message, reason.diagnostic_code())
697            }
698
699            SubtypeFailureReason::IndexSignatureMismatch {
700                index_kind,
701                source_value_type,
702                target_value_type,
703            } => {
704                let source_str = self.format_type(*source_value_type);
705                let target_str = self.format_type(*target_value_type);
706                let message = format!(
707                    "{index_kind} index signature is incompatible: '{source_str}' is not assignable to '{target_str}'."
708                );
709                Diagnostic::error(file_name, start, length, message, reason.diagnostic_code())
710            }
711
712            SubtypeFailureReason::NoUnionMemberMatches {
713                source_type,
714                target_union_members: _,
715            } => {
716                let source_str = self.format_type(*source_type);
717                let target_str = self.format_type(target);
718                let message = format_message(
719                    diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
720                    &[&source_str, &target_str],
721                );
722                Diagnostic::error(
723                    file_name,
724                    start,
725                    length,
726                    message,
727                    diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
728                )
729            }
730
731            SubtypeFailureReason::NoCommonProperties {
732                source_type: _,
733                target_type: _,
734            } => {
735                let source_str = self.format_type_for_assignability_message(source);
736                let target_str = self.format_type_for_assignability_message(target);
737                let message = format_message(
738                    diagnostic_messages::TYPE_HAS_NO_PROPERTIES_IN_COMMON_WITH_TYPE,
739                    &[&source_str, &target_str],
740                );
741                Diagnostic::error(
742                    file_name,
743                    start,
744                    length,
745                    message,
746                    diagnostic_codes::TYPE_HAS_NO_PROPERTIES_IN_COMMON_WITH_TYPE,
747                )
748            }
749
750            SubtypeFailureReason::TypeMismatch {
751                source_type: _,
752                target_type: _,
753            } => {
754                let source_str = self.format_type_for_assignability_message(source);
755                let target_str = self.format_type_for_assignability_message(target);
756
757                if depth == 0
758                    && (target_str == "Callable" || target_str == "Applicable")
759                    && !tsz_solver::is_primitive_type(self.ctx.types, source)
760                {
761                    let prop_name = if target_str == "Callable" {
762                        "call"
763                    } else {
764                        "apply"
765                    };
766                    let message = format_message(
767                        diagnostic_messages::PROPERTY_IS_MISSING_IN_TYPE_BUT_REQUIRED_IN_TYPE,
768                        &[prop_name, &source_str, &target_str],
769                    );
770                    return Diagnostic::error(
771                        file_name,
772                        start,
773                        length,
774                        message,
775                        diagnostic_codes::PROPERTY_IS_MISSING_IN_TYPE_BUT_REQUIRED_IN_TYPE,
776                    );
777                }
778
779                if depth == 0
780                    && let Some(property_name) =
781                        self.missing_single_required_property(source, target)
782                {
783                    let prop_name = self.ctx.types.resolve_atom_ref(property_name);
784                    let message = format_message(
785                        diagnostic_messages::PROPERTY_IS_MISSING_IN_TYPE_BUT_REQUIRED_IN_TYPE,
786                        &[&prop_name, &source_str, &target_str],
787                    );
788                    return Diagnostic::error(
789                        file_name,
790                        start,
791                        length,
792                        message,
793                        diagnostic_codes::PROPERTY_IS_MISSING_IN_TYPE_BUT_REQUIRED_IN_TYPE,
794                    );
795                }
796
797                let base = format_message(
798                    diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
799                    &[&source_str, &target_str],
800                );
801
802                if depth == 0 {
803                    let nonpublic = self.first_nonpublic_constructor_param_property(target);
804                    if tracing::enabled!(Level::TRACE) {
805                        trace!(
806                            target = %target_str,
807                            nonpublic = ?nonpublic,
808                            "nonpublic constructor param property probe"
809                        );
810                    }
811                    if nonpublic.is_some() {
812                        // TODO: tsc emits constructor param visibility as related information
813                        return Diagnostic::error(
814                            file_name,
815                            start,
816                            length,
817                            base,
818                            diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
819                        );
820                    }
821                }
822
823                // TODO: tsc would emit elaboration from elaborate_type_mismatch_detail as related info
824                Diagnostic::error(
825                    file_name,
826                    start,
827                    length,
828                    base,
829                    diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
830                )
831            }
832
833            _ => {
834                // All remaining variants produce a generic "Type X is not assignable to type Y"
835                // with TS2322 code. This covers: PropertyVisibilityMismatch,
836                // PropertyNominalMismatch, ParameterTypeMismatch, NoIntersectionMemberMatches,
837                // IntrinsicTypeMismatch, LiteralTypeMismatch, ErrorType,
838                // RecursionLimitExceeded, ParameterCountMismatch.
839                let source_str = self.format_type_for_assignability_message(source);
840                let target_str = self.format_type_for_assignability_message(target);
841                let message = format_message(
842                    diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
843                    &[&source_str, &target_str],
844                );
845                Diagnostic::error(
846                    file_name,
847                    start,
848                    length,
849                    message,
850                    diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
851                )
852            }
853        }
854    }
855
856    /// Report a type not assignable error with detailed elaboration.
857    ///
858    /// This method uses the solver's "explain" API to determine WHY the types
859    /// are incompatible (e.g., missing property, incompatible property types,
860    /// etc.) and produces a richer diagnostic with that information.
861    ///
862    /// **Architecture Note**: This follows the "Check Fast, Explain Slow" pattern.
863    /// The `is_assignable_to` check is fast (boolean). This explain call is slower
864    /// but produces better error messages. Only call this after a failed check.
865    pub fn error_type_not_assignable_with_reason_at(
866        &mut self,
867        source: TypeId,
868        target: TypeId,
869        idx: NodeIndex,
870    ) {
871        self.diagnose_assignment_failure(source, target, idx);
872    }
873
874    /// Report constructor accessibility mismatch error.
875    pub(crate) fn error_constructor_accessibility_not_assignable(
876        &mut self,
877        source: TypeId,
878        target: TypeId,
879        source_level: Option<MemberAccessLevel>,
880        target_level: Option<MemberAccessLevel>,
881        idx: NodeIndex,
882    ) {
883        let Some(loc) = self.get_source_location(idx) else {
884            return;
885        };
886
887        let source_type = self.format_type(source);
888        let target_type = self.format_type(target);
889        let message = format_message(
890            diagnostic_messages::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
891            &[&source_type, &target_type],
892        );
893        let detail = format!(
894            "Cannot assign a '{}' constructor type to a '{}' constructor type.",
895            Self::constructor_access_name(source_level),
896            Self::constructor_access_name(target_level),
897        );
898
899        let diag = Diagnostic::error(
900            self.ctx.file_name.clone(),
901            loc.start,
902            loc.length(),
903            message,
904            diagnostic_codes::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE,
905        )
906        .with_related(self.ctx.file_name.clone(), loc.start, loc.length(), detail);
907        self.ctx.diagnostics.push(diag);
908    }
909}