Skip to main content

tsz_solver/diagnostics/
mod.rs

1//! Diagnostic core types for the solver.
2//!
3//! This module defines the core data types for type checking diagnostics:
4//!
5//! - **Tracer pattern** (`SubtypeTracer`, `DynSubtypeTracer`): Zero-cost
6//!   abstraction for tracing subtype check failures without logic drift.
7//! - **Failure reasons** (`SubtypeFailureReason`): Structured enum capturing all the
8//!   ways a subtype check can fail.
9//! - **Lazy diagnostics** (`PendingDiagnostic`, `DiagnosticArg`): Deferred formatting
10//!   to avoid expensive `type_to_string()` calls in tentative contexts.
11//! - **Diagnostic codes** (`codes`): TypeScript error code aliases.
12//! - **Data types** (`TypeDiagnostic`, `SourceSpan`, etc.): Core diagnostic structures.
13//!
14//! For eagerly-rendered diagnostic builders, see [`builders`].
15
16pub mod builders;
17
18use crate::types::{TypeId, Visibility};
19use std::sync::Arc;
20use tsz_binder::SymbolId;
21use tsz_common::interner::Atom;
22
23// =============================================================================
24// Tracer Pattern: Zero-Cost Diagnostic Abstraction
25// =============================================================================
26
27/// A trait for tracing subtype check failures.
28///
29/// This trait enables the same subtype checking logic to be used for both
30/// fast boolean checks and detailed diagnostics.
31///
32/// The key insight is that failure reasons are constructed lazily via a closure,
33/// so fast-path implementations can skip the allocation entirely while diagnostic
34/// implementations collect detailed information.
35///
36/// # Example
37///
38/// ```rust,ignore
39/// fn check_subtype_with_tracer<T: SubtypeTracer>(
40///     source: TypeId,
41///     target: TypeId,
42///     tracer: &mut T,
43/// ) -> bool {
44///     if source == target {
45///         return true;
46///     }
47///     tracer.on_mismatch(|| SubtypeFailureReason::TypeMismatch { source, target })
48/// }
49/// ```
50pub trait SubtypeTracer {
51    /// Called when a subtype mismatch is detected.
52    ///
53    /// The `reason` closure is only called if the tracer needs to collect
54    /// the failure reason, allowing fast-path implementations to skip
55    /// allocation entirely.
56    ///
57    /// # Returns
58    ///
59    /// - `true` if checking should continue (for collecting more nested failures)
60    /// - `false` if checking should stop immediately (fast path)
61    ///
62    /// # Type Parameters
63    ///
64    /// The `reason` parameter is a closure that constructs the failure reason.
65    /// It's wrapped in `FnOnce` so it's only called when needed.
66    fn on_mismatch(&mut self, reason: impl FnOnce() -> SubtypeFailureReason) -> bool;
67}
68
69/// Object-safe version of `SubtypeTracer` for dynamic dispatch.
70///
71/// This trait is dyn-compatible and can be used as `&mut dyn DynSubtypeTracer`.
72/// It has a simpler signature that takes the reason directly rather than a closure.
73pub trait DynSubtypeTracer {
74    /// Called when a subtype mismatch is detected.
75    ///
76    /// Unlike `SubtypeTracer::on_mismatch`, this takes the reason directly
77    /// rather than a closure. This makes it object-safe (dyn-compatible).
78    ///
79    /// # Returns
80    ///
81    /// - `true` if checking should continue (for collecting more nested failures)
82    /// - `false` if checking should stop immediately (fast path)
83    fn on_mismatch_dyn(&mut self, reason: SubtypeFailureReason) -> bool;
84}
85
86/// Blanket implementation for all `SubtypeTracer` types.
87impl<T: SubtypeTracer> DynSubtypeTracer for T {
88    fn on_mismatch_dyn(&mut self, reason: SubtypeFailureReason) -> bool {
89        self.on_mismatch(|| reason)
90    }
91}
92
93/// Detailed reason for a subtype check failure.
94///
95/// This enum captures all the different ways a subtype check can fail,
96/// with enough detail to generate helpful error messages.
97///
98/// # Nesting
99///
100/// Some variants include `nested_reason` to capture failures in nested types.
101/// For example, a property type mismatch might include why the property types
102/// themselves don't match.
103#[derive(Clone, Debug, PartialEq)]
104pub enum SubtypeFailureReason {
105    /// A required property is missing in the source type.
106    MissingProperty {
107        property_name: Atom,
108        source_type: TypeId,
109        target_type: TypeId,
110    },
111    /// Multiple required properties are missing in the source type (TS2739).
112    MissingProperties {
113        property_names: Vec<Atom>,
114        source_type: TypeId,
115        target_type: TypeId,
116    },
117    /// Property types are incompatible.
118    PropertyTypeMismatch {
119        property_name: Atom,
120        source_property_type: TypeId,
121        target_property_type: TypeId,
122        nested_reason: Option<Box<Self>>,
123    },
124    /// Optional property cannot satisfy required property.
125    OptionalPropertyRequired { property_name: Atom },
126    /// Readonly property cannot satisfy mutable property.
127    ReadonlyPropertyMismatch { property_name: Atom },
128    /// Property visibility mismatch (private/protected vs public).
129    PropertyVisibilityMismatch {
130        property_name: Atom,
131        source_visibility: Visibility,
132        target_visibility: Visibility,
133    },
134    /// Property nominal mismatch (separate declarations of private/protected property).
135    PropertyNominalMismatch { property_name: Atom },
136    /// Return types are incompatible.
137    ReturnTypeMismatch {
138        source_return: TypeId,
139        target_return: TypeId,
140        nested_reason: Option<Box<Self>>,
141    },
142    /// Parameter types are incompatible.
143    ParameterTypeMismatch {
144        param_index: usize,
145        source_param: TypeId,
146        target_param: TypeId,
147    },
148    /// Too many parameters in source.
149    TooManyParameters {
150        source_count: usize,
151        target_count: usize,
152    },
153    /// Tuple element count mismatch.
154    TupleElementMismatch {
155        source_count: usize,
156        target_count: usize,
157    },
158    /// Tuple element type mismatch.
159    TupleElementTypeMismatch {
160        index: usize,
161        source_element: TypeId,
162        target_element: TypeId,
163    },
164    /// Array element type mismatch.
165    ArrayElementMismatch {
166        source_element: TypeId,
167        target_element: TypeId,
168    },
169    /// Index signature value type mismatch.
170    IndexSignatureMismatch {
171        index_kind: &'static str, // "string" or "number"
172        source_value_type: TypeId,
173        target_value_type: TypeId,
174    },
175    /// Missing index signature.
176    MissingIndexSignature { index_kind: &'static str },
177    /// No union member matches.
178    NoUnionMemberMatches {
179        source_type: TypeId,
180        target_union_members: Vec<TypeId>,
181    },
182    /// No intersection member matches target (intersection requires at least one member).
183    NoIntersectionMemberMatches {
184        source_type: TypeId,
185        target_type: TypeId,
186    },
187    /// No overlapping properties for weak type target.
188    NoCommonProperties {
189        source_type: TypeId,
190        target_type: TypeId,
191    },
192    /// Generic type mismatch (no more specific reason).
193    TypeMismatch {
194        source_type: TypeId,
195        target_type: TypeId,
196    },
197    /// Intrinsic type mismatch (e.g., string vs number).
198    IntrinsicTypeMismatch {
199        source_type: TypeId,
200        target_type: TypeId,
201    },
202    /// Literal type mismatch (e.g., "hello" vs "world" or "hello" vs 42).
203    LiteralTypeMismatch {
204        source_type: TypeId,
205        target_type: TypeId,
206    },
207    /// Error type encountered - indicates unresolved type that should not be silently compatible.
208    ErrorType {
209        source_type: TypeId,
210        target_type: TypeId,
211    },
212    /// Recursion limit exceeded during type checking.
213    RecursionLimitExceeded,
214    /// Parameter count mismatch.
215    ParameterCountMismatch {
216        source_count: usize,
217        target_count: usize,
218    },
219    /// Excess property in object literal assignment (TS2353).
220    ExcessProperty {
221        property_name: Atom,
222        target_type: TypeId,
223    },
224}
225
226/// Diagnostic severity level.
227#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228pub enum DiagnosticSeverity {
229    Error,
230    Warning,
231    Suggestion,
232    Message,
233}
234
235// =============================================================================
236// Lazy Diagnostic Arguments
237// =============================================================================
238
239/// Argument for a diagnostic message template.
240///
241/// Instead of eagerly formatting types to strings, we store the raw data
242/// (`TypeId`, `SymbolId`, etc.) and only format when rendering.
243#[derive(Clone, Debug)]
244pub enum DiagnosticArg {
245    /// A type reference (will be formatted via `TypeFormatter`)
246    Type(TypeId),
247    /// A symbol reference (will be looked up by name)
248    Symbol(SymbolId),
249    /// An interned string
250    Atom(Atom),
251    /// A plain string
252    String(Arc<str>),
253    /// A number
254    Number(usize),
255}
256
257macro_rules! impl_from_diagnostic_arg {
258    ($($source:ty => $variant:ident),* $(,)?) => {
259        $(impl From<$source> for DiagnosticArg {
260            fn from(v: $source) -> Self { Self::$variant(v) }
261        })*
262    };
263}
264
265impl_from_diagnostic_arg! {
266    TypeId   => Type,
267    SymbolId => Symbol,
268    Atom     => Atom,
269    usize    => Number,
270}
271
272impl From<&str> for DiagnosticArg {
273    fn from(s: &str) -> Self {
274        Self::String(s.into())
275    }
276}
277
278impl From<String> for DiagnosticArg {
279    fn from(s: String) -> Self {
280        Self::String(s.into())
281    }
282}
283
284/// A pending diagnostic that hasn't been rendered yet.
285///
286/// This stores the structured data needed to generate an error message,
287/// but defers the expensive string formatting until rendering time.
288#[derive(Clone, Debug)]
289pub struct PendingDiagnostic {
290    /// Diagnostic code (e.g., 2322 for type not assignable)
291    pub code: u32,
292    /// Arguments for the message template
293    pub args: Vec<DiagnosticArg>,
294    /// Primary source location
295    pub span: Option<SourceSpan>,
296    /// Severity level
297    pub severity: DiagnosticSeverity,
298    /// Related information (additional locations)
299    pub related: Vec<Self>,
300}
301
302impl PendingDiagnostic {
303    /// Create a new pending error diagnostic.
304    pub const fn error(code: u32, args: Vec<DiagnosticArg>) -> Self {
305        Self {
306            code,
307            args,
308            span: None,
309            severity: DiagnosticSeverity::Error,
310            related: Vec::new(),
311        }
312    }
313
314    /// Attach a source span to this diagnostic.
315    pub fn with_span(mut self, span: SourceSpan) -> Self {
316        self.span = Some(span);
317        self
318    }
319
320    /// Add related information.
321    pub fn with_related(mut self, related: Self) -> Self {
322        self.related.push(related);
323        self
324    }
325}
326
327/// A source location span.
328#[derive(Clone, Debug, PartialEq, Eq)]
329pub struct SourceSpan {
330    /// Start position (byte offset)
331    pub start: u32,
332    /// Length in bytes
333    pub length: u32,
334    /// File path or name
335    pub file: Arc<str>,
336}
337
338impl SourceSpan {
339    pub fn new(file: impl Into<Arc<str>>, start: u32, length: u32) -> Self {
340        Self {
341            start,
342            length,
343            file: file.into(),
344        }
345    }
346}
347
348/// Related diagnostic information (e.g., "see declaration here").
349#[derive(Clone, Debug)]
350pub struct RelatedInformation {
351    pub span: SourceSpan,
352    pub message: String,
353}
354
355/// A type checking diagnostic.
356#[derive(Clone, Debug)]
357pub struct TypeDiagnostic {
358    /// The main error message
359    pub message: String,
360    /// Diagnostic code (e.g., 2322 for "Type X is not assignable to type Y")
361    pub code: u32,
362    /// Severity level
363    pub severity: DiagnosticSeverity,
364    /// Primary source location
365    pub span: Option<SourceSpan>,
366    /// Related information (additional locations)
367    pub related: Vec<RelatedInformation>,
368}
369
370impl TypeDiagnostic {
371    /// Create a new error diagnostic.
372    pub fn error(message: impl Into<String>, code: u32) -> Self {
373        Self {
374            message: message.into(),
375            code,
376            severity: DiagnosticSeverity::Error,
377            span: None,
378            related: Vec::new(),
379        }
380    }
381
382    /// Add a source span to this diagnostic.
383    pub fn with_span(mut self, span: SourceSpan) -> Self {
384        self.span = Some(span);
385        self
386    }
387
388    /// Add related information.
389    pub fn with_related(mut self, span: SourceSpan, message: impl Into<String>) -> Self {
390        self.related.push(RelatedInformation {
391            span,
392            message: message.into(),
393        });
394        self
395    }
396}
397
398// =============================================================================
399// Diagnostic Codes (matching TypeScript's)
400// =============================================================================
401
402/// TypeScript diagnostic codes for type errors.
403///
404/// These are re-exported from `tsz_common::diagnostics::diagnostic_codes` with
405/// short aliases for ergonomic use within the solver. The canonical definitions
406/// live in `tsz-common` to maintain a single source of truth.
407pub mod codes {
408    use tsz_common::diagnostics::diagnostic_codes as dc;
409
410    // Type assignability
411    pub use dc::ARGUMENT_OF_TYPE_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE as ARG_NOT_ASSIGNABLE;
412    pub use dc::CANNOT_ASSIGN_TO_BECAUSE_IT_IS_A_READ_ONLY_PROPERTY as READONLY_PROPERTY;
413    pub use dc::OBJECT_LITERAL_MAY_ONLY_SPECIFY_KNOWN_PROPERTIES_AND_DOES_NOT_EXIST_IN_TYPE as EXCESS_PROPERTY;
414    pub use dc::PROPERTY_IS_MISSING_IN_TYPE_BUT_REQUIRED_IN_TYPE as PROPERTY_MISSING;
415    pub use dc::PROPERTY_IS_PRIVATE_AND_ONLY_ACCESSIBLE_WITHIN_CLASS as PROPERTY_VISIBILITY_MISMATCH;
416    pub use dc::PROPERTY_IS_PROTECTED_AND_ONLY_ACCESSIBLE_THROUGH_AN_INSTANCE_OF_CLASS_THIS_IS_A as PROPERTY_NOMINAL_MISMATCH;
417    pub use dc::TYPE_HAS_NO_PROPERTIES_IN_COMMON_WITH_TYPE as NO_COMMON_PROPERTIES;
418    pub use dc::TYPE_IS_MISSING_THE_FOLLOWING_PROPERTIES_FROM_TYPE as MISSING_PROPERTIES;
419    pub use dc::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE as TYPE_NOT_ASSIGNABLE;
420
421    pub use dc::INDEX_SIGNATURE_FOR_TYPE_IS_MISSING_IN_TYPE as MISSING_INDEX_SIGNATURE;
422    pub use dc::TYPES_OF_PROPERTY_ARE_INCOMPATIBLE as PROPERTY_TYPE_MISMATCH;
423
424    // Function/call errors
425    pub use dc::CANNOT_FIND_NAME;
426    pub use dc::CANNOT_FIND_NAME_DO_YOU_NEED_TO_CHANGE_YOUR_TARGET_LIBRARY_TRY_CHANGING_THE_LIB as CANNOT_FIND_NAME_TARGET_LIB;
427    pub use dc::CANNOT_FIND_NAME_DO_YOU_NEED_TO_CHANGE_YOUR_TARGET_LIBRARY_TRY_CHANGING_THE_LIB_2 as CANNOT_FIND_NAME_DOM;
428    pub use dc::CANNOT_FIND_NAME_DO_YOU_NEED_TO_INSTALL_TYPE_DEFINITIONS_FOR_A_TEST_RUNNER_TRY_N as CANNOT_FIND_NAME_TEST_RUNNER;
429    pub use dc::CANNOT_FIND_NAME_DO_YOU_NEED_TO_INSTALL_TYPE_DEFINITIONS_FOR_NODE_TRY_NPM_I_SAVE as CANNOT_FIND_NAME_NODE;
430    pub use dc::EXPECTED_ARGUMENTS_BUT_GOT as ARG_COUNT_MISMATCH;
431    pub use dc::PROPERTY_DOES_NOT_EXIST_ON_TYPE as PROPERTY_NOT_EXIST;
432    pub use dc::PROPERTY_DOES_NOT_EXIST_ON_TYPE_DID_YOU_MEAN as PROPERTY_NOT_EXIST_DID_YOU_MEAN;
433    pub use dc::THE_THIS_CONTEXT_OF_TYPE_IS_NOT_ASSIGNABLE_TO_METHODS_THIS_OF_TYPE as THIS_TYPE_MISMATCH;
434    pub use dc::THIS_EXPRESSION_IS_NOT_CALLABLE as NOT_CALLABLE;
435
436    // Null/undefined errors
437
438    // Implicit any errors (7xxx series)
439    pub use dc::FUNCTION_EXPRESSION_WHICH_LACKS_RETURN_TYPE_ANNOTATION_IMPLICITLY_HAS_AN_RETURN as IMPLICIT_ANY_RETURN_FUNCTION_EXPRESSION;
440    pub use dc::MEMBER_IMPLICITLY_HAS_AN_TYPE as IMPLICIT_ANY_MEMBER;
441    pub use dc::PARAMETER_IMPLICITLY_HAS_AN_TYPE as IMPLICIT_ANY_PARAMETER;
442    pub use dc::VARIABLE_IMPLICITLY_HAS_AN_TYPE as IMPLICIT_ANY;
443    pub use dc::WHICH_LACKS_RETURN_TYPE_ANNOTATION_IMPLICITLY_HAS_AN_RETURN_TYPE as IMPLICIT_ANY_RETURN;
444}
445
446/// Map well-known names to their specialized "cannot find name" diagnostic codes.
447///
448/// TypeScript emits different error codes for well-known globals that are missing
449/// because they require specific type definitions or target library changes:
450/// - Node.js globals (require, process, Buffer, etc.) → TS2580
451/// - Test runner globals (describe, it, test, etc.) → TS2582
452/// - Target library types (Promise, Symbol, Map, etc.) → TS2583
453/// - DOM globals (document, console) → TS2584
454pub(crate) fn cannot_find_name_code(name: &str) -> u32 {
455    match name {
456        // Node.js globals → TS2580
457        "require" | "exports" | "module" | "process" | "Buffer" | "__filename" | "__dirname" => {
458            codes::CANNOT_FIND_NAME_NODE
459        }
460        // Test runner globals → TS2582
461        "describe" | "suite" | "it" | "test" => codes::CANNOT_FIND_NAME_TEST_RUNNER,
462        // Target library types → TS2583
463        "Promise" | "Symbol" | "Map" | "Set" | "Reflect" | "Iterator" | "AsyncIterator"
464        | "SharedArrayBuffer" => codes::CANNOT_FIND_NAME_TARGET_LIB,
465        // DOM globals → TS2584
466        "document" | "console" => codes::CANNOT_FIND_NAME_DOM,
467        // Everything else → TS2304
468        _ => codes::CANNOT_FIND_NAME,
469    }
470}
471
472// =============================================================================
473// Message Templates
474// =============================================================================
475
476/// Get the message template for a diagnostic code.
477///
478/// Templates use {0}, {1}, etc. as placeholders for arguments.
479/// Message strings are sourced from `tsz_common::diagnostics::diagnostic_messages`
480/// to maintain a single source of truth with the checker.
481pub fn get_message_template(code: u32) -> &'static str {
482    tsz_common::diagnostics::get_message_template(code).unwrap_or("Unknown diagnostic")
483}
484
485// =============================================================================
486// Pending Diagnostic Builder (LAZY)
487// =============================================================================
488
489/// Builder for creating lazy pending diagnostics.
490///
491/// This builder creates `PendingDiagnostic` instances that defer expensive
492/// string formatting until rendering time.
493pub struct PendingDiagnosticBuilder;
494
495// =============================================================================
496// SubtypeFailureReason to PendingDiagnostic Conversion
497// =============================================================================
498
499impl SubtypeFailureReason {
500    /// Return the primary diagnostic code for this failure reason.
501    ///
502    /// This is the single source of truth for mapping `SubtypeFailureReason` variants
503    /// to diagnostic codes. Both the solver's `to_diagnostic` and the checker's
504    /// `render_failure_reason` should use this to stay in sync.
505    pub const fn diagnostic_code(&self) -> u32 {
506        match self {
507            Self::MissingProperty { .. } | Self::OptionalPropertyRequired { .. } => {
508                codes::PROPERTY_MISSING
509            }
510            Self::MissingProperties { .. } => codes::MISSING_PROPERTIES,
511            Self::PropertyTypeMismatch { .. } => codes::PROPERTY_TYPE_MISMATCH,
512            Self::ReadonlyPropertyMismatch { .. } => codes::READONLY_PROPERTY,
513            Self::PropertyVisibilityMismatch { .. } => codes::PROPERTY_VISIBILITY_MISMATCH,
514            Self::PropertyNominalMismatch { .. } => codes::PROPERTY_NOMINAL_MISMATCH,
515            Self::ReturnTypeMismatch { .. }
516            | Self::ParameterTypeMismatch { .. }
517            | Self::TupleElementMismatch { .. }
518            | Self::TupleElementTypeMismatch { .. }
519            | Self::ArrayElementMismatch { .. }
520            | Self::IndexSignatureMismatch { .. }
521            | Self::MissingIndexSignature { .. }
522            | Self::NoUnionMemberMatches { .. }
523            | Self::NoIntersectionMemberMatches { .. }
524            | Self::TypeMismatch { .. }
525            | Self::IntrinsicTypeMismatch { .. }
526            | Self::LiteralTypeMismatch { .. }
527            | Self::ErrorType { .. }
528            | Self::RecursionLimitExceeded
529            | Self::ParameterCountMismatch { .. } => codes::TYPE_NOT_ASSIGNABLE,
530            Self::TooManyParameters { .. } => codes::ARG_COUNT_MISMATCH,
531            Self::NoCommonProperties { .. } => codes::NO_COMMON_PROPERTIES,
532            Self::ExcessProperty { .. } => codes::EXCESS_PROPERTY,
533        }
534    }
535
536    /// Convert this failure reason to a `PendingDiagnostic`.
537    ///
538    /// This is the "explain slow" path - called only when we need to report
539    /// an error and want a detailed message about why the type check failed.
540    pub fn to_diagnostic(&self, source: TypeId, target: TypeId) -> PendingDiagnostic {
541        match self {
542            Self::MissingProperty {
543                property_name,
544                source_type,
545                target_type,
546            } => PendingDiagnostic::error(
547                codes::PROPERTY_MISSING,
548                vec![
549                    (*property_name).into(),
550                    (*source_type).into(),
551                    (*target_type).into(),
552                ],
553            ),
554
555            Self::MissingProperties {
556                property_names: _,
557                source_type,
558                target_type,
559            } => PendingDiagnostic::error(
560                codes::MISSING_PROPERTIES,
561                vec![(*source_type).into(), (*target_type).into()],
562            ),
563
564            Self::PropertyTypeMismatch {
565                property_name,
566                source_property_type,
567                target_property_type,
568                nested_reason,
569            } => {
570                // Main error: Type not assignable
571                let mut diag = PendingDiagnostic::error(
572                    codes::TYPE_NOT_ASSIGNABLE,
573                    vec![source.into(), target.into()],
574                );
575
576                // Add elaboration: Types of property 'x' are incompatible (TS2326)
577                let elaboration = PendingDiagnostic::error(
578                    codes::PROPERTY_TYPE_MISMATCH,
579                    vec![(*property_name).into()],
580                );
581                diag = diag.with_related(elaboration);
582
583                // If there's a nested reason, add that too
584                if let Some(nested) = nested_reason {
585                    let nested_diag =
586                        nested.to_diagnostic(*source_property_type, *target_property_type);
587                    diag = diag.with_related(nested_diag);
588                }
589
590                diag
591            }
592
593            Self::OptionalPropertyRequired { property_name } => {
594                // This is a specific case of type not assignable
595                PendingDiagnostic::error(
596                    codes::TYPE_NOT_ASSIGNABLE,
597                    vec![source.into(), target.into()],
598                )
599                .with_related(PendingDiagnostic::error(
600                    codes::PROPERTY_MISSING, // Close enough - property is "missing" because it's optional
601                    vec![(*property_name).into(), source.into(), target.into()],
602                ))
603            }
604
605            Self::ReadonlyPropertyMismatch { property_name } => PendingDiagnostic::error(
606                codes::TYPE_NOT_ASSIGNABLE,
607                vec![source.into(), target.into()],
608            )
609            .with_related(PendingDiagnostic::error(
610                codes::READONLY_PROPERTY,
611                vec![(*property_name).into()],
612            )),
613
614            Self::PropertyVisibilityMismatch {
615                property_name,
616                source_visibility,
617                target_visibility,
618            } => {
619                // TS2341/TS2445: Property 'x' is private in type 'A' but not in type 'B'
620                PendingDiagnostic::error(
621                    codes::TYPE_NOT_ASSIGNABLE,
622                    vec![source.into(), target.into()],
623                )
624                .with_related(PendingDiagnostic::error(
625                    codes::PROPERTY_VISIBILITY_MISMATCH,
626                    vec![
627                        (*property_name).into(),
628                        format!("{source_visibility:?}").into(),
629                        format!("{target_visibility:?}").into(),
630                    ],
631                ))
632            }
633
634            Self::PropertyNominalMismatch { property_name } => {
635                // TS2446: Types have separate declarations of a private property 'x'
636                PendingDiagnostic::error(
637                    codes::TYPE_NOT_ASSIGNABLE,
638                    vec![source.into(), target.into()],
639                )
640                .with_related(PendingDiagnostic::error(
641                    codes::PROPERTY_NOMINAL_MISMATCH,
642                    vec![(*property_name).into()],
643                ))
644            }
645
646            Self::ReturnTypeMismatch {
647                source_return,
648                target_return,
649                nested_reason,
650            } => {
651                let mut diag = PendingDiagnostic::error(
652                    codes::TYPE_NOT_ASSIGNABLE,
653                    vec![source.into(), target.into()],
654                );
655
656                // Add: Type 'X' is not assignable to type 'Y' (for return types)
657                let return_diag = PendingDiagnostic::error(
658                    codes::TYPE_NOT_ASSIGNABLE,
659                    vec![(*source_return).into(), (*target_return).into()],
660                );
661                diag = diag.with_related(return_diag);
662
663                if let Some(nested) = nested_reason {
664                    let nested_diag = nested.to_diagnostic(*source_return, *target_return);
665                    diag = diag.with_related(nested_diag);
666                }
667
668                diag
669            }
670
671            Self::ParameterTypeMismatch {
672                param_index: _,
673                source_param,
674                target_param,
675            } => PendingDiagnostic::error(
676                codes::TYPE_NOT_ASSIGNABLE,
677                vec![source.into(), target.into()],
678            )
679            .with_related(PendingDiagnostic::error(
680                codes::TYPE_NOT_ASSIGNABLE,
681                vec![(*source_param).into(), (*target_param).into()],
682            )),
683
684            Self::TooManyParameters {
685                source_count,
686                target_count,
687            } => PendingDiagnostic::error(
688                codes::ARG_COUNT_MISMATCH,
689                vec![(*target_count).into(), (*source_count).into()],
690            ),
691
692            Self::TupleElementMismatch {
693                source_count,
694                target_count,
695            } => PendingDiagnostic::error(
696                codes::TYPE_NOT_ASSIGNABLE,
697                vec![source.into(), target.into()],
698            )
699            .with_related(PendingDiagnostic::error(
700                codes::ARG_COUNT_MISMATCH,
701                vec![(*target_count).into(), (*source_count).into()],
702            )),
703
704            Self::TupleElementTypeMismatch {
705                index: _,
706                source_element,
707                target_element,
708            }
709            | Self::ArrayElementMismatch {
710                source_element,
711                target_element,
712            } => PendingDiagnostic::error(
713                codes::TYPE_NOT_ASSIGNABLE,
714                vec![source.into(), target.into()],
715            )
716            .with_related(PendingDiagnostic::error(
717                codes::TYPE_NOT_ASSIGNABLE,
718                vec![(*source_element).into(), (*target_element).into()],
719            )),
720
721            Self::IndexSignatureMismatch {
722                index_kind: _,
723                source_value_type,
724                target_value_type,
725            } => PendingDiagnostic::error(
726                codes::TYPE_NOT_ASSIGNABLE,
727                vec![source.into(), target.into()],
728            )
729            .with_related(PendingDiagnostic::error(
730                codes::TYPE_NOT_ASSIGNABLE,
731                vec![(*source_value_type).into(), (*target_value_type).into()],
732            )),
733
734            Self::MissingIndexSignature { index_kind } => PendingDiagnostic::error(
735                codes::TYPE_NOT_ASSIGNABLE,
736                vec![source.into(), target.into()],
737            )
738            .with_related(PendingDiagnostic::error(
739                codes::MISSING_INDEX_SIGNATURE,
740                vec![index_kind.to_string().into(), source.into()],
741            )),
742
743            Self::NoUnionMemberMatches {
744                source_type,
745                target_union_members,
746            } => {
747                const UNION_MEMBER_DIAGNOSTIC_LIMIT: usize = 3;
748                let mut diag = PendingDiagnostic::error(
749                    codes::TYPE_NOT_ASSIGNABLE,
750                    vec![(*source_type).into(), target.into()],
751                );
752                for member in target_union_members
753                    .iter()
754                    .take(UNION_MEMBER_DIAGNOSTIC_LIMIT)
755                {
756                    diag.related.push(PendingDiagnostic::error(
757                        codes::TYPE_NOT_ASSIGNABLE,
758                        vec![(*source_type).into(), (*member).into()],
759                    ));
760                }
761                diag
762            }
763
764            Self::NoIntersectionMemberMatches {
765                source_type,
766                target_type,
767            }
768            | Self::TypeMismatch {
769                source_type,
770                target_type,
771            }
772            | Self::IntrinsicTypeMismatch {
773                source_type,
774                target_type,
775            }
776            | Self::LiteralTypeMismatch {
777                source_type,
778                target_type,
779            }
780            | Self::ErrorType {
781                source_type,
782                target_type,
783            } => PendingDiagnostic::error(
784                codes::TYPE_NOT_ASSIGNABLE,
785                vec![(*source_type).into(), (*target_type).into()],
786            ),
787
788            Self::NoCommonProperties {
789                source_type,
790                target_type,
791            } => PendingDiagnostic::error(
792                codes::NO_COMMON_PROPERTIES,
793                vec![(*source_type).into(), (*target_type).into()],
794            ),
795
796            Self::RecursionLimitExceeded => {
797                // Recursion limit - use the source/target from the call site
798                PendingDiagnostic::error(
799                    codes::TYPE_NOT_ASSIGNABLE,
800                    vec![source.into(), target.into()],
801                )
802            }
803
804            Self::ParameterCountMismatch {
805                source_count: _,
806                target_count: _,
807            } => {
808                // Parameter count mismatch
809                PendingDiagnostic::error(
810                    codes::TYPE_NOT_ASSIGNABLE,
811                    vec![source.into(), target.into()],
812                )
813            }
814
815            Self::ExcessProperty {
816                property_name,
817                target_type,
818            } => {
819                // TS2353: Object literal may only specify known properties
820                PendingDiagnostic::error(
821                    codes::EXCESS_PROPERTY,
822                    vec![(*property_name).into(), (*target_type).into()],
823                )
824            }
825        }
826    }
827}
828
829impl PendingDiagnosticBuilder {
830    /// Create an "Argument not assignable" pending diagnostic.
831    pub fn argument_not_assignable(arg_type: TypeId, param_type: TypeId) -> PendingDiagnostic {
832        PendingDiagnostic::error(
833            codes::ARG_NOT_ASSIGNABLE,
834            vec![arg_type.into(), param_type.into()],
835        )
836    }
837
838    /// Create an "Expected N arguments but got M" pending diagnostic.
839    pub fn argument_count_mismatch(expected: usize, got: usize) -> PendingDiagnostic {
840        PendingDiagnostic::error(codes::ARG_COUNT_MISMATCH, vec![expected.into(), got.into()])
841    }
842}
843
844#[cfg(test)]
845impl PendingDiagnosticBuilder {
846    /// Create a "Type X is not assignable to type Y" pending diagnostic.
847    pub fn type_not_assignable(source: TypeId, target: TypeId) -> PendingDiagnostic {
848        PendingDiagnostic::error(
849            codes::TYPE_NOT_ASSIGNABLE,
850            vec![source.into(), target.into()],
851        )
852    }
853
854    /// Create a "Property X is missing" pending diagnostic.
855    pub fn property_missing(prop_name: &str, source: TypeId, target: TypeId) -> PendingDiagnostic {
856        PendingDiagnostic::error(
857            codes::PROPERTY_MISSING,
858            vec![prop_name.into(), source.into(), target.into()],
859        )
860    }
861
862    /// Create a "Property X does not exist" pending diagnostic.
863    pub fn property_not_exist(prop_name: &str, type_id: TypeId) -> PendingDiagnostic {
864        PendingDiagnostic::error(
865            codes::PROPERTY_NOT_EXIST,
866            vec![prop_name.into(), type_id.into()],
867        )
868    }
869
870    /// Create a "Cannot find name" pending diagnostic.
871    pub fn cannot_find_name(name: &str) -> PendingDiagnostic {
872        let code = cannot_find_name_code(name);
873        PendingDiagnostic::error(code, vec![name.into()])
874    }
875
876    /// Create a "Type is not callable" pending diagnostic.
877    pub fn not_callable(type_id: TypeId) -> PendingDiagnostic {
878        PendingDiagnostic::error(codes::NOT_CALLABLE, vec![type_id.into()])
879    }
880
881    pub fn this_type_mismatch(expected_this: TypeId, actual_this: TypeId) -> PendingDiagnostic {
882        PendingDiagnostic::error(
883            codes::THIS_TYPE_MISMATCH,
884            vec![actual_this.into(), expected_this.into()],
885        )
886    }
887
888    /// Create a "Cannot assign to readonly property" pending diagnostic.
889    pub fn readonly_property(prop_name: &str) -> PendingDiagnostic {
890        PendingDiagnostic::error(codes::READONLY_PROPERTY, vec![prop_name.into()])
891    }
892
893    /// Create an "Excess property" pending diagnostic.
894    pub fn excess_property(prop_name: &str, target: TypeId) -> PendingDiagnostic {
895        PendingDiagnostic::error(
896            codes::EXCESS_PROPERTY,
897            vec![prop_name.into(), target.into()],
898        )
899    }
900}
901
902#[cfg(test)]
903use crate::types::*;
904
905#[cfg(test)]
906#[path = "../../tests/diagnostics_tests.rs"]
907mod tests;