Skip to main content

tsz_solver/
diagnostics.rs

1//! Diagnostic generation for the solver.
2//!
3//! This module provides error message generation for type checking failures.
4//! It produces human-readable diagnostics with source locations and context.
5//!
6//! ## Architecture: Lazy Diagnostics
7//!
8//! To avoid expensive string formatting during type checking (especially in tentative
9//! contexts like overload resolution), this module uses a two-phase approach:
10//!
11//! 1. **Collection**: Store structured data in `PendingDiagnostic` with `DiagnosticArg` values
12//! 2. **Rendering**: Format strings lazily only when displaying to the user
13//!
14//! This prevents calling `type_to_string()` thousands of times for errors that are
15//! discarded during overload resolution.
16//!
17//! ## Tracer Pattern (Zero-Cost Abstraction)
18//!
19//! The tracer pattern allows the same subtype checking logic to be used for both
20//! fast boolean checks and detailed diagnostic generation, eliminating logic drift.
21//!
22//! - **`FastTracer`**: Zero-cost abstraction that compiles to a simple boolean return
23//! - **`DiagnosticTracer`**: Collects detailed `SubtypeFailureReason` for error messages
24
25use crate::TypeDatabase;
26use crate::TypeFormatter;
27use crate::def::DefinitionStore;
28use crate::types::{TypeId, Visibility};
29use std::sync::Arc;
30use tsz_binder::SymbolId;
31use tsz_common::interner::Atom;
32
33// =============================================================================
34// Tracer Pattern: Zero-Cost Diagnostic Abstraction
35// =============================================================================
36
37/// A trait for tracing subtype check failures.
38///
39/// This trait enables the same subtype checking logic to be used for both
40/// fast boolean checks (via `FastTracer`) and detailed diagnostics (via `DiagnosticTracer`).
41///
42/// The key insight is that failure reasons are constructed lazily via a closure,
43/// so `FastTracer` can skip the allocation entirely while `DiagnosticTracer` collects
44/// detailed information.
45///
46/// # Example
47///
48/// ```rust,ignore
49/// fn check_subtype_with_tracer<T: SubtypeTracer>(
50///     source: TypeId,
51///     target: TypeId,
52///     tracer: &mut T,
53/// ) -> bool {
54///     if source == target {
55///         return true;
56///     }
57///     tracer.on_mismatch(|| SubtypeFailureReason::TypeMismatch { source, target })
58/// }
59/// ```
60pub trait SubtypeTracer {
61    /// Called when a subtype mismatch is detected.
62    ///
63    /// The `reason` closure is only called if the tracer needs to collect
64    /// the failure reason. This allows `FastTracer` to skip the allocation
65    /// entirely while `DiagnosticTracer` can collect detailed information.
66    ///
67    /// # Returns
68    ///
69    /// - `true` if checking should continue (for collecting more nested failures)
70    /// - `false` if checking should stop immediately (fast path)
71    ///
72    /// # Type Parameters
73    ///
74    /// The `reason` parameter is a closure that constructs the failure reason.
75    /// It's wrapped in `FnOnce` so it's only called when needed.
76    fn on_mismatch(&mut self, reason: impl FnOnce() -> SubtypeFailureReason) -> bool;
77}
78
79/// Object-safe version of `SubtypeTracer` for dynamic dispatch.
80///
81/// This trait is dyn-compatible and can be used as `&mut dyn DynSubtypeTracer`.
82/// It has a simpler signature that takes the reason directly rather than a closure.
83pub trait DynSubtypeTracer {
84    /// Called when a subtype mismatch is detected.
85    ///
86    /// Unlike `SubtypeTracer::on_mismatch`, this takes the reason directly
87    /// rather than a closure. This makes it object-safe (dyn-compatible).
88    ///
89    /// # Returns
90    ///
91    /// - `true` if checking should continue (for collecting more nested failures)
92    /// - `false` if checking should stop immediately (fast path)
93    fn on_mismatch_dyn(&mut self, reason: SubtypeFailureReason) -> bool;
94}
95
96/// Blanket implementation for all `SubtypeTracer` types.
97impl<T: SubtypeTracer> DynSubtypeTracer for T {
98    fn on_mismatch_dyn(&mut self, reason: SubtypeFailureReason) -> bool {
99        self.on_mismatch(|| reason)
100    }
101}
102
103#[cfg(test)]
104/// Fast tracer that returns immediately on mismatch (zero-cost abstraction).
105///
106/// This tracer is used for fast subtype checks where we only care about the
107/// boolean result. The `#[inline(always)]` attribute ensures that this compiles
108/// to the same code as a simple `return false` statement with no runtime overhead.
109///
110/// # Zero-Cost Abstraction
111///
112/// ```rust,ignore
113/// // With FastTracer, this compiles to:
114/// // if condition { return false; }
115/// if !tracer.on_mismatch(|| reason) { return false; }
116/// ```
117///
118/// The closure is never called, so no allocations occur.
119#[derive(Clone, Copy, Debug)]
120pub struct FastTracer;
121
122#[cfg(test)]
123impl SubtypeTracer for FastTracer {
124    /// Always return `false` to stop checking immediately.
125    ///
126    /// The `reason` closure is never called, so no `SubtypeFailureReason` is constructed.
127    /// This is the zero-cost path - the compiler will optimize this to a simple boolean return.
128    #[inline(always)]
129    fn on_mismatch(&mut self, _reason: impl FnOnce() -> SubtypeFailureReason) -> bool {
130        false
131    }
132}
133
134#[cfg(test)]
135/// Diagnostic tracer that collects detailed failure reasons.
136///
137/// This tracer is used when we need to generate detailed error messages.
138/// It collects the first `SubtypeFailureReason` encountered and stops checking.
139///
140/// # Example
141///
142/// ```rust,ignore
143/// let mut tracer = DiagnosticTracer::new();
144/// check_subtype_with_tracer(source, target, &mut tracer);
145/// if let Some(reason) = tracer.take_failure() {
146///     // Generate error message from reason
147/// }
148/// ```
149#[derive(Debug)]
150pub struct DiagnosticTracer {
151    /// The first failure reason encountered (if any).
152    failure: Option<SubtypeFailureReason>,
153}
154
155#[cfg(test)]
156impl DiagnosticTracer {
157    /// Create a new diagnostic tracer.
158    pub fn new() -> Self {
159        Self { failure: None }
160    }
161
162    /// Take the collected failure reason, leaving `None` in its place.
163    pub fn take_failure(&mut self) -> Option<SubtypeFailureReason> {
164        self.failure.take()
165    }
166
167    /// Get a reference to the collected failure reason (if any).
168    /// Check if any failure was collected.
169    pub fn has_failure(&self) -> bool {
170        self.failure.is_some()
171    }
172}
173
174#[cfg(test)]
175impl Default for DiagnosticTracer {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181#[cfg(test)]
182impl SubtypeTracer for DiagnosticTracer {
183    /// Collect the failure reason and stop checking.
184    ///
185    /// The `reason` closure is called to construct the detailed failure reason,
186    /// which is stored for later use in error message generation.
187    ///
188    /// Returns `false` to stop checking after collecting the first failure.
189    /// This matches the semantics of `FastTracer` while collecting diagnostics.
190    #[inline]
191    fn on_mismatch(&mut self, reason: impl FnOnce() -> SubtypeFailureReason) -> bool {
192        // Only collect the first failure (subsequent failures are nested details)
193        if self.failure.is_none() {
194            self.failure = Some(reason());
195        }
196        false
197    }
198}
199
200/// Detailed reason for a subtype check failure.
201///
202/// This enum captures all the different ways a subtype check can fail,
203/// with enough detail to generate helpful error messages.
204///
205/// # Nesting
206///
207/// Some variants include `nested_reason` to capture failures in nested types.
208/// For example, a property type mismatch might include why the property types
209/// themselves don't match.
210#[derive(Clone, Debug, PartialEq)]
211pub enum SubtypeFailureReason {
212    /// A required property is missing in the source type.
213    MissingProperty {
214        property_name: Atom,
215        source_type: TypeId,
216        target_type: TypeId,
217    },
218    /// Multiple required properties are missing in the source type (TS2739).
219    MissingProperties {
220        property_names: Vec<Atom>,
221        source_type: TypeId,
222        target_type: TypeId,
223    },
224    /// Property types are incompatible.
225    PropertyTypeMismatch {
226        property_name: Atom,
227        source_property_type: TypeId,
228        target_property_type: TypeId,
229        nested_reason: Option<Box<Self>>,
230    },
231    /// Optional property cannot satisfy required property.
232    OptionalPropertyRequired { property_name: Atom },
233    /// Readonly property cannot satisfy mutable property.
234    ReadonlyPropertyMismatch { property_name: Atom },
235    /// Property visibility mismatch (private/protected vs public).
236    PropertyVisibilityMismatch {
237        property_name: Atom,
238        source_visibility: Visibility,
239        target_visibility: Visibility,
240    },
241    /// Property nominal mismatch (separate declarations of private/protected property).
242    PropertyNominalMismatch { property_name: Atom },
243    /// Return types are incompatible.
244    ReturnTypeMismatch {
245        source_return: TypeId,
246        target_return: TypeId,
247        nested_reason: Option<Box<Self>>,
248    },
249    /// Parameter types are incompatible.
250    ParameterTypeMismatch {
251        param_index: usize,
252        source_param: TypeId,
253        target_param: TypeId,
254    },
255    /// Too many parameters in source.
256    TooManyParameters {
257        source_count: usize,
258        target_count: usize,
259    },
260    /// Tuple element count mismatch.
261    TupleElementMismatch {
262        source_count: usize,
263        target_count: usize,
264    },
265    /// Tuple element type mismatch.
266    TupleElementTypeMismatch {
267        index: usize,
268        source_element: TypeId,
269        target_element: TypeId,
270    },
271    /// Array element type mismatch.
272    ArrayElementMismatch {
273        source_element: TypeId,
274        target_element: TypeId,
275    },
276    /// Index signature value type mismatch.
277    IndexSignatureMismatch {
278        index_kind: &'static str, // "string" or "number"
279        source_value_type: TypeId,
280        target_value_type: TypeId,
281    },
282    /// Missing index signature.
283    MissingIndexSignature { index_kind: &'static str },
284    /// No union member matches.
285    NoUnionMemberMatches {
286        source_type: TypeId,
287        target_union_members: Vec<TypeId>,
288    },
289    /// No intersection member matches target (intersection requires at least one member).
290    NoIntersectionMemberMatches {
291        source_type: TypeId,
292        target_type: TypeId,
293    },
294    /// No overlapping properties for weak type target.
295    NoCommonProperties {
296        source_type: TypeId,
297        target_type: TypeId,
298    },
299    /// Generic type mismatch (no more specific reason).
300    TypeMismatch {
301        source_type: TypeId,
302        target_type: TypeId,
303    },
304    /// Intrinsic type mismatch (e.g., string vs number).
305    IntrinsicTypeMismatch {
306        source_type: TypeId,
307        target_type: TypeId,
308    },
309    /// Literal type mismatch (e.g., "hello" vs "world" or "hello" vs 42).
310    LiteralTypeMismatch {
311        source_type: TypeId,
312        target_type: TypeId,
313    },
314    /// Error type encountered - indicates unresolved type that should not be silently compatible.
315    ErrorType {
316        source_type: TypeId,
317        target_type: TypeId,
318    },
319    /// Recursion limit exceeded during type checking.
320    RecursionLimitExceeded,
321    /// Parameter count mismatch.
322    ParameterCountMismatch {
323        source_count: usize,
324        target_count: usize,
325    },
326    /// Excess property in object literal assignment (TS2353).
327    ExcessProperty {
328        property_name: Atom,
329        target_type: TypeId,
330    },
331}
332
333/// Diagnostic severity level.
334#[derive(Clone, Copy, Debug, PartialEq, Eq)]
335pub enum DiagnosticSeverity {
336    Error,
337    Warning,
338    Suggestion,
339    Message,
340}
341
342// =============================================================================
343// Lazy Diagnostic Arguments
344// =============================================================================
345
346/// Argument for a diagnostic message template.
347///
348/// Instead of eagerly formatting types to strings, we store the raw data
349/// (`TypeId`, `SymbolId`, etc.) and only format when rendering.
350#[derive(Clone, Debug)]
351pub enum DiagnosticArg {
352    /// A type reference (will be formatted via `TypeFormatter`)
353    Type(TypeId),
354    /// A symbol reference (will be looked up by name)
355    Symbol(SymbolId),
356    /// An interned string
357    Atom(Atom),
358    /// A plain string
359    String(Arc<str>),
360    /// A number
361    Number(usize),
362}
363
364macro_rules! impl_from_diagnostic_arg {
365    ($($source:ty => $variant:ident),* $(,)?) => {
366        $(impl From<$source> for DiagnosticArg {
367            fn from(v: $source) -> Self { Self::$variant(v) }
368        })*
369    };
370}
371
372impl_from_diagnostic_arg! {
373    TypeId   => Type,
374    SymbolId => Symbol,
375    Atom     => Atom,
376    usize    => Number,
377}
378
379impl From<&str> for DiagnosticArg {
380    fn from(s: &str) -> Self {
381        Self::String(s.into())
382    }
383}
384
385impl From<String> for DiagnosticArg {
386    fn from(s: String) -> Self {
387        Self::String(s.into())
388    }
389}
390
391/// A pending diagnostic that hasn't been rendered yet.
392///
393/// This stores the structured data needed to generate an error message,
394/// but defers the expensive string formatting until rendering time.
395#[derive(Clone, Debug)]
396pub struct PendingDiagnostic {
397    /// Diagnostic code (e.g., 2322 for type not assignable)
398    pub code: u32,
399    /// Arguments for the message template
400    pub args: Vec<DiagnosticArg>,
401    /// Primary source location
402    pub span: Option<SourceSpan>,
403    /// Severity level
404    pub severity: DiagnosticSeverity,
405    /// Related information (additional locations)
406    pub related: Vec<Self>,
407}
408
409impl PendingDiagnostic {
410    /// Create a new pending error diagnostic.
411    pub const fn error(code: u32, args: Vec<DiagnosticArg>) -> Self {
412        Self {
413            code,
414            args,
415            span: None,
416            severity: DiagnosticSeverity::Error,
417            related: Vec::new(),
418        }
419    }
420
421    /// Attach a source span to this diagnostic.
422    pub fn with_span(mut self, span: SourceSpan) -> Self {
423        self.span = Some(span);
424        self
425    }
426
427    /// Add related information.
428    pub fn with_related(mut self, related: Self) -> Self {
429        self.related.push(related);
430        self
431    }
432}
433
434/// A source location span.
435#[derive(Clone, Debug, PartialEq, Eq)]
436pub struct SourceSpan {
437    /// Start position (byte offset)
438    pub start: u32,
439    /// Length in bytes
440    pub length: u32,
441    /// File path or name
442    pub file: Arc<str>,
443}
444
445impl SourceSpan {
446    pub fn new(file: impl Into<Arc<str>>, start: u32, length: u32) -> Self {
447        Self {
448            start,
449            length,
450            file: file.into(),
451        }
452    }
453}
454
455/// Related diagnostic information (e.g., "see declaration here").
456#[derive(Clone, Debug)]
457pub struct RelatedInformation {
458    pub span: SourceSpan,
459    pub message: String,
460}
461
462/// A type checking diagnostic.
463#[derive(Clone, Debug)]
464pub struct TypeDiagnostic {
465    /// The main error message
466    pub message: String,
467    /// Diagnostic code (e.g., 2322 for "Type X is not assignable to type Y")
468    pub code: u32,
469    /// Severity level
470    pub severity: DiagnosticSeverity,
471    /// Primary source location
472    pub span: Option<SourceSpan>,
473    /// Related information (additional locations)
474    pub related: Vec<RelatedInformation>,
475}
476
477impl TypeDiagnostic {
478    /// Create a new error diagnostic.
479    pub fn error(message: impl Into<String>, code: u32) -> Self {
480        Self {
481            message: message.into(),
482            code,
483            severity: DiagnosticSeverity::Error,
484            span: None,
485            related: Vec::new(),
486        }
487    }
488
489    /// Add a source span to this diagnostic.
490    pub fn with_span(mut self, span: SourceSpan) -> Self {
491        self.span = Some(span);
492        self
493    }
494
495    /// Add related information.
496    pub fn with_related(mut self, span: SourceSpan, message: impl Into<String>) -> Self {
497        self.related.push(RelatedInformation {
498            span,
499            message: message.into(),
500        });
501        self
502    }
503}
504
505// =============================================================================
506// Diagnostic Codes (matching TypeScript's)
507// =============================================================================
508
509/// TypeScript diagnostic codes for type errors.
510///
511/// These are re-exported from `tsz_common::diagnostics::diagnostic_codes` with
512/// short aliases for ergonomic use within the solver. The canonical definitions
513/// live in `tsz-common` to maintain a single source of truth.
514pub mod codes {
515    use tsz_common::diagnostics::diagnostic_codes as dc;
516
517    // Type assignability
518    pub use dc::ARGUMENT_OF_TYPE_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE as ARG_NOT_ASSIGNABLE;
519    pub use dc::CANNOT_ASSIGN_TO_BECAUSE_IT_IS_A_READ_ONLY_PROPERTY as READONLY_PROPERTY;
520    pub use dc::OBJECT_LITERAL_MAY_ONLY_SPECIFY_KNOWN_PROPERTIES_AND_DOES_NOT_EXIST_IN_TYPE as EXCESS_PROPERTY;
521    pub use dc::PROPERTY_IS_MISSING_IN_TYPE_BUT_REQUIRED_IN_TYPE as PROPERTY_MISSING;
522    pub use dc::PROPERTY_IS_PRIVATE_AND_ONLY_ACCESSIBLE_WITHIN_CLASS as PROPERTY_VISIBILITY_MISMATCH;
523    pub use dc::PROPERTY_IS_PROTECTED_AND_ONLY_ACCESSIBLE_THROUGH_AN_INSTANCE_OF_CLASS_THIS_IS_A as PROPERTY_NOMINAL_MISMATCH;
524    pub use dc::TYPE_HAS_NO_PROPERTIES_IN_COMMON_WITH_TYPE as NO_COMMON_PROPERTIES;
525    pub use dc::TYPE_IS_MISSING_THE_FOLLOWING_PROPERTIES_FROM_TYPE as MISSING_PROPERTIES;
526    pub use dc::TYPE_IS_NOT_ASSIGNABLE_TO_TYPE as TYPE_NOT_ASSIGNABLE;
527
528    pub use dc::INDEX_SIGNATURE_FOR_TYPE_IS_MISSING_IN_TYPE as MISSING_INDEX_SIGNATURE;
529    pub use dc::TYPES_OF_PROPERTY_ARE_INCOMPATIBLE as PROPERTY_TYPE_MISMATCH;
530
531    // Function/call errors
532    pub use dc::CANNOT_FIND_NAME;
533    pub use dc::CANNOT_FIND_NAME_DO_YOU_NEED_TO_CHANGE_YOUR_TARGET_LIBRARY_TRY_CHANGING_THE_LIB as CANNOT_FIND_NAME_TARGET_LIB;
534    pub use dc::CANNOT_FIND_NAME_DO_YOU_NEED_TO_CHANGE_YOUR_TARGET_LIBRARY_TRY_CHANGING_THE_LIB_2 as CANNOT_FIND_NAME_DOM;
535    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;
536    pub use dc::CANNOT_FIND_NAME_DO_YOU_NEED_TO_INSTALL_TYPE_DEFINITIONS_FOR_NODE_TRY_NPM_I_SAVE as CANNOT_FIND_NAME_NODE;
537    pub use dc::EXPECTED_ARGUMENTS_BUT_GOT as ARG_COUNT_MISMATCH;
538    pub use dc::PROPERTY_DOES_NOT_EXIST_ON_TYPE as PROPERTY_NOT_EXIST;
539    pub use dc::PROPERTY_DOES_NOT_EXIST_ON_TYPE_DID_YOU_MEAN as PROPERTY_NOT_EXIST_DID_YOU_MEAN;
540    pub use dc::THE_THIS_CONTEXT_OF_TYPE_IS_NOT_ASSIGNABLE_TO_METHODS_THIS_OF_TYPE as THIS_TYPE_MISMATCH;
541    pub use dc::THIS_EXPRESSION_IS_NOT_CALLABLE as NOT_CALLABLE;
542
543    // Null/undefined errors
544
545    // Implicit any errors (7xxx series)
546    pub use dc::FUNCTION_EXPRESSION_WHICH_LACKS_RETURN_TYPE_ANNOTATION_IMPLICITLY_HAS_AN_RETURN as IMPLICIT_ANY_RETURN_FUNCTION_EXPRESSION;
547    pub use dc::MEMBER_IMPLICITLY_HAS_AN_TYPE as IMPLICIT_ANY_MEMBER;
548    pub use dc::PARAMETER_IMPLICITLY_HAS_AN_TYPE as IMPLICIT_ANY_PARAMETER;
549    pub use dc::VARIABLE_IMPLICITLY_HAS_AN_TYPE as IMPLICIT_ANY;
550    pub use dc::WHICH_LACKS_RETURN_TYPE_ANNOTATION_IMPLICITLY_HAS_AN_RETURN_TYPE as IMPLICIT_ANY_RETURN;
551}
552
553/// Map well-known names to their specialized "cannot find name" diagnostic codes.
554///
555/// TypeScript emits different error codes for well-known globals that are missing
556/// because they require specific type definitions or target library changes:
557/// - Node.js globals (require, process, Buffer, etc.) → TS2580
558/// - Test runner globals (describe, it, test, etc.) → TS2582
559/// - Target library types (Promise, Symbol, Map, etc.) → TS2583
560/// - DOM globals (document, console) → TS2584
561fn cannot_find_name_code(name: &str) -> u32 {
562    match name {
563        // Node.js globals → TS2580
564        "require" | "exports" | "module" | "process" | "Buffer" | "__filename" | "__dirname" => {
565            codes::CANNOT_FIND_NAME_NODE
566        }
567        // Test runner globals → TS2582
568        "describe" | "suite" | "it" | "test" => codes::CANNOT_FIND_NAME_TEST_RUNNER,
569        // Target library types → TS2583
570        "Promise" | "Symbol" | "Map" | "Set" | "Reflect" | "Iterator" | "AsyncIterator"
571        | "SharedArrayBuffer" => codes::CANNOT_FIND_NAME_TARGET_LIB,
572        // DOM globals → TS2584
573        "document" | "console" => codes::CANNOT_FIND_NAME_DOM,
574        // Everything else → TS2304
575        _ => codes::CANNOT_FIND_NAME,
576    }
577}
578
579// =============================================================================
580// Message Templates
581// =============================================================================
582
583/// Get the message template for a diagnostic code.
584///
585/// Templates use {0}, {1}, etc. as placeholders for arguments.
586/// Message strings are sourced from `tsz_common::diagnostics::diagnostic_messages`
587/// to maintain a single source of truth with the checker.
588pub fn get_message_template(code: u32) -> &'static str {
589    tsz_common::diagnostics::get_message_template(code).unwrap_or("Unknown diagnostic")
590}
591
592// =============================================================================
593// Type Formatting
594// =============================================================================
595
596// TypeFormatter is now in format.rs
597
598// Diagnostic Builder
599// =============================================================================
600
601/// Builder for creating type error diagnostics.
602pub struct DiagnosticBuilder<'a> {
603    formatter: TypeFormatter<'a>,
604}
605
606impl<'a> DiagnosticBuilder<'a> {
607    pub fn new(interner: &'a dyn TypeDatabase) -> Self {
608        DiagnosticBuilder {
609            formatter: TypeFormatter::new(interner),
610        }
611    }
612
613    /// Create a diagnostic builder with access to symbol names.
614    ///
615    /// This prevents "Ref(N)" fallback strings in diagnostic messages by
616    /// resolving symbol references to their actual names.
617    pub fn with_symbols(
618        interner: &'a dyn TypeDatabase,
619        symbol_arena: &'a tsz_binder::SymbolArena,
620    ) -> Self {
621        DiagnosticBuilder {
622            formatter: TypeFormatter::with_symbols(interner, symbol_arena),
623        }
624    }
625
626    /// Create a diagnostic builder with access to definition store.
627    ///
628    /// This prevents "Lazy(N)" fallback strings in diagnostic messages by
629    /// resolving `DefIds` to their type names.
630    pub fn with_def_store(mut self, def_store: &'a DefinitionStore) -> Self {
631        self.formatter = self.formatter.with_def_store(def_store);
632        self
633    }
634
635    /// Create a "Type X is not assignable to type Y" diagnostic.
636    pub fn type_not_assignable(&mut self, source: TypeId, target: TypeId) -> TypeDiagnostic {
637        let source_str = self.formatter.format(source);
638        let target_str = self.formatter.format(target);
639        TypeDiagnostic::error(
640            format!("Type '{source_str}' is not assignable to type '{target_str}'."),
641            codes::TYPE_NOT_ASSIGNABLE,
642        )
643    }
644
645    /// Create a "Property X is missing in type Y" diagnostic.
646    pub fn property_missing(
647        &mut self,
648        prop_name: &str,
649        source: TypeId,
650        target: TypeId,
651    ) -> TypeDiagnostic {
652        let source_str = self.formatter.format(source);
653        let target_str = self.formatter.format(target);
654        TypeDiagnostic::error(
655            format!(
656                "Property '{prop_name}' is missing in type '{source_str}' but required in type '{target_str}'."
657            ),
658            codes::PROPERTY_MISSING,
659        )
660    }
661
662    /// Create a "Property X does not exist on type Y" diagnostic.
663    pub fn property_not_exist(&mut self, prop_name: &str, type_id: TypeId) -> TypeDiagnostic {
664        let type_str = self.formatter.format(type_id);
665        TypeDiagnostic::error(
666            format!("Property '{prop_name}' does not exist on type '{type_str}'."),
667            codes::PROPERTY_NOT_EXIST,
668        )
669    }
670
671    /// Create a "Property X does not exist on type Y. Did you mean Z?" diagnostic (TS2551).
672    pub fn property_not_exist_did_you_mean(
673        &mut self,
674        prop_name: &str,
675        type_id: TypeId,
676        suggestion: &str,
677    ) -> TypeDiagnostic {
678        let type_str = self.formatter.format(type_id);
679        TypeDiagnostic::error(
680            format!(
681                "Property '{prop_name}' does not exist on type '{type_str}'. Did you mean '{suggestion}'?"
682            ),
683            codes::PROPERTY_NOT_EXIST_DID_YOU_MEAN,
684        )
685    }
686
687    /// Create an "Argument not assignable" diagnostic.
688    pub fn argument_not_assignable(
689        &mut self,
690        arg_type: TypeId,
691        param_type: TypeId,
692    ) -> TypeDiagnostic {
693        let arg_str = self.formatter.format(arg_type);
694        let param_str = self.formatter.format(param_type);
695        TypeDiagnostic::error(
696            format!(
697                "Argument of type '{arg_str}' is not assignable to parameter of type '{param_str}'."
698            ),
699            codes::ARG_NOT_ASSIGNABLE,
700        )
701    }
702
703    /// Create a "Cannot find name" diagnostic.
704    pub fn cannot_find_name(&mut self, name: &str) -> TypeDiagnostic {
705        // Skip TS2304 for identifiers that are clearly not valid names.
706        // These are likely parse errors (e.g., ",", ";", "(") that were
707        // added to the AST for error recovery. The parse error should have
708        // already been emitted (e.g., TS1136 "Property assignment expected").
709        let is_obviously_invalid = name.len() == 1
710            && matches!(
711                name.chars().next(),
712                Some(
713                    ',' | ';'
714                        | ':'
715                        | '('
716                        | ')'
717                        | '['
718                        | ']'
719                        | '{'
720                        | '}'
721                        | '+'
722                        | '-'
723                        | '*'
724                        | '/'
725                        | '%'
726                        | '&'
727                        | '|'
728                        | '^'
729                        | '!'
730                        | '~'
731                        | '<'
732                        | '>'
733                        | '='
734                        | '.'
735                )
736            );
737
738        if is_obviously_invalid {
739            // Return a dummy diagnostic with empty message that will be ignored
740            return TypeDiagnostic::error("", 0);
741        }
742
743        let code = cannot_find_name_code(name);
744        TypeDiagnostic::error(format!("Cannot find name '{name}'."), code)
745    }
746
747    /// Create a "Type X is not callable" diagnostic.
748    pub fn not_callable(&mut self, type_id: TypeId) -> TypeDiagnostic {
749        let type_str = self.formatter.format(type_id);
750        TypeDiagnostic::error(
751            format!("Type '{type_str}' has no call signatures."),
752            codes::NOT_CALLABLE,
753        )
754    }
755
756    pub fn this_type_mismatch(
757        &mut self,
758        expected_this: TypeId,
759        actual_this: TypeId,
760    ) -> TypeDiagnostic {
761        let expected_str = self.formatter.format(expected_this);
762        let actual_str = self.formatter.format(actual_this);
763        TypeDiagnostic::error(
764            format!(
765                "The 'this' context of type '{actual_str}' is not assignable to method's 'this' of type '{expected_str}'."
766            ),
767            codes::THIS_TYPE_MISMATCH,
768        )
769    }
770
771    /// Create an "Expected N arguments but got M" diagnostic.
772    pub fn argument_count_mismatch(&mut self, expected: usize, got: usize) -> TypeDiagnostic {
773        TypeDiagnostic::error(
774            format!("Expected {expected} arguments, but got {got}."),
775            codes::ARG_COUNT_MISMATCH,
776        )
777    }
778
779    /// Create a "Cannot assign to readonly property" diagnostic.
780    pub fn readonly_property(&mut self, prop_name: &str) -> TypeDiagnostic {
781        TypeDiagnostic::error(
782            format!("Cannot assign to '{prop_name}' because it is a read-only property."),
783            codes::READONLY_PROPERTY,
784        )
785    }
786
787    /// Create an "Excess property" diagnostic.
788    pub fn excess_property(&mut self, prop_name: &str, target: TypeId) -> TypeDiagnostic {
789        let target_str = self.formatter.format(target);
790        TypeDiagnostic::error(
791            format!(
792                "Object literal may only specify known properties, and '{prop_name}' does not exist in type '{target_str}'."
793            ),
794            codes::EXCESS_PROPERTY,
795        )
796    }
797
798    // =========================================================================
799    // Implicit Any Diagnostics (TS7006, TS7008, TS7010, TS7011)
800    // =========================================================================
801
802    /// Create a "Parameter implicitly has an 'any' type" diagnostic (TS7006).
803    ///
804    /// This is emitted when noImplicitAny is enabled and a function parameter
805    /// has no type annotation and no contextual type.
806    pub fn implicit_any_parameter(&mut self, param_name: &str) -> TypeDiagnostic {
807        TypeDiagnostic::error(
808            format!("Parameter '{param_name}' implicitly has an 'any' type."),
809            codes::IMPLICIT_ANY_PARAMETER,
810        )
811    }
812
813    /// Create a "Parameter implicitly has a specific type" diagnostic (TS7006 variant).
814    ///
815    /// This is used when the implicit type is known to be something other than 'any',
816    /// such as when a rest parameter implicitly has 'any[]'.
817    pub fn implicit_any_parameter_with_type(
818        &mut self,
819        param_name: &str,
820        implicit_type: TypeId,
821    ) -> TypeDiagnostic {
822        let type_str = self.formatter.format(implicit_type);
823        TypeDiagnostic::error(
824            format!("Parameter '{param_name}' implicitly has an '{type_str}' type."),
825            codes::IMPLICIT_ANY_PARAMETER,
826        )
827    }
828
829    /// Create a "Member implicitly has an 'any' type" diagnostic (TS7008).
830    ///
831    /// This is emitted when noImplicitAny is enabled and a class/interface member
832    /// has no type annotation.
833    pub fn implicit_any_member(&mut self, member_name: &str) -> TypeDiagnostic {
834        TypeDiagnostic::error(
835            format!("Member '{member_name}' implicitly has an 'any' type."),
836            codes::IMPLICIT_ANY_MEMBER,
837        )
838    }
839
840    /// Create a "Variable implicitly has an 'any' type" diagnostic (TS7005).
841    ///
842    /// This is emitted when noImplicitAny is enabled and a variable declaration
843    /// has no type annotation and the inferred type is 'any'.
844    pub fn implicit_any_variable(&mut self, var_name: &str, var_type: TypeId) -> TypeDiagnostic {
845        let type_str = self.formatter.format(var_type);
846        TypeDiagnostic::error(
847            format!("Variable '{var_name}' implicitly has an '{type_str}' type."),
848            codes::IMPLICIT_ANY,
849        )
850    }
851
852    /// Create an "implicitly has an 'any' return type" diagnostic (TS7010).
853    ///
854    /// This is emitted when noImplicitAny is enabled and a function declaration
855    /// has no return type annotation and returns 'any'.
856    pub fn implicit_any_return(&mut self, func_name: &str, return_type: TypeId) -> TypeDiagnostic {
857        let type_str = self.formatter.format(return_type);
858        TypeDiagnostic::error(
859            format!(
860                "'{func_name}', which lacks return-type annotation, implicitly has an '{type_str}' return type."
861            ),
862            codes::IMPLICIT_ANY_RETURN,
863        )
864    }
865
866    /// Create a "Function expression implicitly has an 'any' return type" diagnostic (TS7011).
867    ///
868    /// This is emitted when noImplicitAny is enabled and a function expression
869    /// has no return type annotation and returns 'any'.
870    pub fn implicit_any_return_function_expression(
871        &mut self,
872        return_type: TypeId,
873    ) -> TypeDiagnostic {
874        let type_str = self.formatter.format(return_type);
875        TypeDiagnostic::error(
876            format!(
877                "Function expression, which lacks return-type annotation, implicitly has an '{type_str}' return type."
878            ),
879            codes::IMPLICIT_ANY_RETURN_FUNCTION_EXPRESSION,
880        )
881    }
882}
883
884// =============================================================================
885// Pending Diagnostic Builder (LAZY)
886// =============================================================================
887
888/// Builder for creating lazy pending diagnostics.
889///
890/// This builder creates `PendingDiagnostic` instances that defer expensive
891/// string formatting until rendering time.
892pub struct PendingDiagnosticBuilder;
893
894// =============================================================================
895// SubtypeFailureReason to PendingDiagnostic Conversion
896// =============================================================================
897
898impl SubtypeFailureReason {
899    /// Return the primary diagnostic code for this failure reason.
900    ///
901    /// This is the single source of truth for mapping `SubtypeFailureReason` variants
902    /// to diagnostic codes. Both the solver's `to_diagnostic` and the checker's
903    /// `render_failure_reason` should use this to stay in sync.
904    pub const fn diagnostic_code(&self) -> u32 {
905        match self {
906            Self::MissingProperty { .. } | Self::OptionalPropertyRequired { .. } => {
907                codes::PROPERTY_MISSING
908            }
909            Self::MissingProperties { .. } => codes::MISSING_PROPERTIES,
910            Self::PropertyTypeMismatch { .. } => codes::PROPERTY_TYPE_MISMATCH,
911            Self::ReadonlyPropertyMismatch { .. } => codes::READONLY_PROPERTY,
912            Self::PropertyVisibilityMismatch { .. } => codes::PROPERTY_VISIBILITY_MISMATCH,
913            Self::PropertyNominalMismatch { .. } => codes::PROPERTY_NOMINAL_MISMATCH,
914            Self::ReturnTypeMismatch { .. }
915            | Self::ParameterTypeMismatch { .. }
916            | Self::TupleElementMismatch { .. }
917            | Self::TupleElementTypeMismatch { .. }
918            | Self::ArrayElementMismatch { .. }
919            | Self::IndexSignatureMismatch { .. }
920            | Self::MissingIndexSignature { .. }
921            | Self::NoUnionMemberMatches { .. }
922            | Self::NoIntersectionMemberMatches { .. }
923            | Self::TypeMismatch { .. }
924            | Self::IntrinsicTypeMismatch { .. }
925            | Self::LiteralTypeMismatch { .. }
926            | Self::ErrorType { .. }
927            | Self::RecursionLimitExceeded
928            | Self::ParameterCountMismatch { .. } => codes::TYPE_NOT_ASSIGNABLE,
929            Self::TooManyParameters { .. } => codes::ARG_COUNT_MISMATCH,
930            Self::NoCommonProperties { .. } => codes::NO_COMMON_PROPERTIES,
931            Self::ExcessProperty { .. } => codes::EXCESS_PROPERTY,
932        }
933    }
934
935    /// Convert this failure reason to a `PendingDiagnostic`.
936    ///
937    /// This is the "explain slow" path - called only when we need to report
938    /// an error and want a detailed message about why the type check failed.
939    pub fn to_diagnostic(&self, source: TypeId, target: TypeId) -> PendingDiagnostic {
940        match self {
941            Self::MissingProperty {
942                property_name,
943                source_type,
944                target_type,
945            } => PendingDiagnostic::error(
946                codes::PROPERTY_MISSING,
947                vec![
948                    (*property_name).into(),
949                    (*source_type).into(),
950                    (*target_type).into(),
951                ],
952            ),
953
954            Self::MissingProperties {
955                property_names: _,
956                source_type,
957                target_type,
958            } => PendingDiagnostic::error(
959                codes::MISSING_PROPERTIES,
960                vec![(*source_type).into(), (*target_type).into()],
961            ),
962
963            Self::PropertyTypeMismatch {
964                property_name,
965                source_property_type,
966                target_property_type,
967                nested_reason,
968            } => {
969                // Main error: Type not assignable
970                let mut diag = PendingDiagnostic::error(
971                    codes::TYPE_NOT_ASSIGNABLE,
972                    vec![source.into(), target.into()],
973                );
974
975                // Add elaboration: Types of property 'x' are incompatible (TS2326)
976                let elaboration = PendingDiagnostic::error(
977                    codes::PROPERTY_TYPE_MISMATCH,
978                    vec![(*property_name).into()],
979                );
980                diag = diag.with_related(elaboration);
981
982                // If there's a nested reason, add that too
983                if let Some(nested) = nested_reason {
984                    let nested_diag =
985                        nested.to_diagnostic(*source_property_type, *target_property_type);
986                    diag = diag.with_related(nested_diag);
987                }
988
989                diag
990            }
991
992            Self::OptionalPropertyRequired { property_name } => {
993                // This is a specific case of type not assignable
994                PendingDiagnostic::error(
995                    codes::TYPE_NOT_ASSIGNABLE,
996                    vec![source.into(), target.into()],
997                )
998                .with_related(PendingDiagnostic::error(
999                    codes::PROPERTY_MISSING, // Close enough - property is "missing" because it's optional
1000                    vec![(*property_name).into(), source.into(), target.into()],
1001                ))
1002            }
1003
1004            Self::ReadonlyPropertyMismatch { property_name } => PendingDiagnostic::error(
1005                codes::TYPE_NOT_ASSIGNABLE,
1006                vec![source.into(), target.into()],
1007            )
1008            .with_related(PendingDiagnostic::error(
1009                codes::READONLY_PROPERTY,
1010                vec![(*property_name).into()],
1011            )),
1012
1013            Self::PropertyVisibilityMismatch {
1014                property_name,
1015                source_visibility,
1016                target_visibility,
1017            } => {
1018                // TS2341/TS2445: Property 'x' is private in type 'A' but not in type 'B'
1019                PendingDiagnostic::error(
1020                    codes::TYPE_NOT_ASSIGNABLE,
1021                    vec![source.into(), target.into()],
1022                )
1023                .with_related(PendingDiagnostic::error(
1024                    codes::PROPERTY_VISIBILITY_MISMATCH,
1025                    vec![
1026                        (*property_name).into(),
1027                        format!("{source_visibility:?}").into(),
1028                        format!("{target_visibility:?}").into(),
1029                    ],
1030                ))
1031            }
1032
1033            Self::PropertyNominalMismatch { property_name } => {
1034                // TS2446: Types have separate declarations of a private property 'x'
1035                PendingDiagnostic::error(
1036                    codes::TYPE_NOT_ASSIGNABLE,
1037                    vec![source.into(), target.into()],
1038                )
1039                .with_related(PendingDiagnostic::error(
1040                    codes::PROPERTY_NOMINAL_MISMATCH,
1041                    vec![(*property_name).into()],
1042                ))
1043            }
1044
1045            Self::ReturnTypeMismatch {
1046                source_return,
1047                target_return,
1048                nested_reason,
1049            } => {
1050                let mut diag = PendingDiagnostic::error(
1051                    codes::TYPE_NOT_ASSIGNABLE,
1052                    vec![source.into(), target.into()],
1053                );
1054
1055                // Add: Type 'X' is not assignable to type 'Y' (for return types)
1056                let return_diag = PendingDiagnostic::error(
1057                    codes::TYPE_NOT_ASSIGNABLE,
1058                    vec![(*source_return).into(), (*target_return).into()],
1059                );
1060                diag = diag.with_related(return_diag);
1061
1062                if let Some(nested) = nested_reason {
1063                    let nested_diag = nested.to_diagnostic(*source_return, *target_return);
1064                    diag = diag.with_related(nested_diag);
1065                }
1066
1067                diag
1068            }
1069
1070            Self::ParameterTypeMismatch {
1071                param_index: _,
1072                source_param,
1073                target_param,
1074            } => PendingDiagnostic::error(
1075                codes::TYPE_NOT_ASSIGNABLE,
1076                vec![source.into(), target.into()],
1077            )
1078            .with_related(PendingDiagnostic::error(
1079                codes::TYPE_NOT_ASSIGNABLE,
1080                vec![(*source_param).into(), (*target_param).into()],
1081            )),
1082
1083            Self::TooManyParameters {
1084                source_count,
1085                target_count,
1086            } => PendingDiagnostic::error(
1087                codes::ARG_COUNT_MISMATCH,
1088                vec![(*target_count).into(), (*source_count).into()],
1089            ),
1090
1091            Self::TupleElementMismatch {
1092                source_count,
1093                target_count,
1094            } => PendingDiagnostic::error(
1095                codes::TYPE_NOT_ASSIGNABLE,
1096                vec![source.into(), target.into()],
1097            )
1098            .with_related(PendingDiagnostic::error(
1099                codes::ARG_COUNT_MISMATCH,
1100                vec![(*target_count).into(), (*source_count).into()],
1101            )),
1102
1103            Self::TupleElementTypeMismatch {
1104                index: _,
1105                source_element,
1106                target_element,
1107            }
1108            | Self::ArrayElementMismatch {
1109                source_element,
1110                target_element,
1111            } => PendingDiagnostic::error(
1112                codes::TYPE_NOT_ASSIGNABLE,
1113                vec![source.into(), target.into()],
1114            )
1115            .with_related(PendingDiagnostic::error(
1116                codes::TYPE_NOT_ASSIGNABLE,
1117                vec![(*source_element).into(), (*target_element).into()],
1118            )),
1119
1120            Self::IndexSignatureMismatch {
1121                index_kind: _,
1122                source_value_type,
1123                target_value_type,
1124            } => PendingDiagnostic::error(
1125                codes::TYPE_NOT_ASSIGNABLE,
1126                vec![source.into(), target.into()],
1127            )
1128            .with_related(PendingDiagnostic::error(
1129                codes::TYPE_NOT_ASSIGNABLE,
1130                vec![(*source_value_type).into(), (*target_value_type).into()],
1131            )),
1132
1133            Self::MissingIndexSignature { index_kind } => PendingDiagnostic::error(
1134                codes::TYPE_NOT_ASSIGNABLE,
1135                vec![source.into(), target.into()],
1136            )
1137            .with_related(PendingDiagnostic::error(
1138                codes::MISSING_INDEX_SIGNATURE,
1139                vec![index_kind.to_string().into(), source.into()],
1140            )),
1141
1142            Self::NoUnionMemberMatches {
1143                source_type,
1144                target_union_members,
1145            } => {
1146                const UNION_MEMBER_DIAGNOSTIC_LIMIT: usize = 3;
1147                let mut diag = PendingDiagnostic::error(
1148                    codes::TYPE_NOT_ASSIGNABLE,
1149                    vec![(*source_type).into(), target.into()],
1150                );
1151                for member in target_union_members
1152                    .iter()
1153                    .take(UNION_MEMBER_DIAGNOSTIC_LIMIT)
1154                {
1155                    diag.related.push(PendingDiagnostic::error(
1156                        codes::TYPE_NOT_ASSIGNABLE,
1157                        vec![(*source_type).into(), (*member).into()],
1158                    ));
1159                }
1160                diag
1161            }
1162
1163            Self::NoIntersectionMemberMatches {
1164                source_type,
1165                target_type,
1166            }
1167            | Self::TypeMismatch {
1168                source_type,
1169                target_type,
1170            }
1171            | Self::IntrinsicTypeMismatch {
1172                source_type,
1173                target_type,
1174            }
1175            | Self::LiteralTypeMismatch {
1176                source_type,
1177                target_type,
1178            }
1179            | Self::ErrorType {
1180                source_type,
1181                target_type,
1182            } => PendingDiagnostic::error(
1183                codes::TYPE_NOT_ASSIGNABLE,
1184                vec![(*source_type).into(), (*target_type).into()],
1185            ),
1186
1187            Self::NoCommonProperties {
1188                source_type,
1189                target_type,
1190            } => PendingDiagnostic::error(
1191                codes::NO_COMMON_PROPERTIES,
1192                vec![(*source_type).into(), (*target_type).into()],
1193            ),
1194
1195            Self::RecursionLimitExceeded => {
1196                // Recursion limit - use the source/target from the call site
1197                PendingDiagnostic::error(
1198                    codes::TYPE_NOT_ASSIGNABLE,
1199                    vec![source.into(), target.into()],
1200                )
1201            }
1202
1203            Self::ParameterCountMismatch {
1204                source_count: _,
1205                target_count: _,
1206            } => {
1207                // Parameter count mismatch
1208                PendingDiagnostic::error(
1209                    codes::TYPE_NOT_ASSIGNABLE,
1210                    vec![source.into(), target.into()],
1211                )
1212            }
1213
1214            Self::ExcessProperty {
1215                property_name,
1216                target_type,
1217            } => {
1218                // TS2353: Object literal may only specify known properties
1219                PendingDiagnostic::error(
1220                    codes::EXCESS_PROPERTY,
1221                    vec![(*property_name).into(), (*target_type).into()],
1222                )
1223            }
1224        }
1225    }
1226}
1227
1228impl PendingDiagnosticBuilder {
1229    /// Create an "Argument not assignable" pending diagnostic.
1230    pub fn argument_not_assignable(arg_type: TypeId, param_type: TypeId) -> PendingDiagnostic {
1231        PendingDiagnostic::error(
1232            codes::ARG_NOT_ASSIGNABLE,
1233            vec![arg_type.into(), param_type.into()],
1234        )
1235    }
1236
1237    /// Create an "Expected N arguments but got M" pending diagnostic.
1238    pub fn argument_count_mismatch(expected: usize, got: usize) -> PendingDiagnostic {
1239        PendingDiagnostic::error(codes::ARG_COUNT_MISMATCH, vec![expected.into(), got.into()])
1240    }
1241}
1242
1243#[cfg(test)]
1244impl PendingDiagnosticBuilder {
1245    /// Create a "Type X is not assignable to type Y" pending diagnostic.
1246    pub fn type_not_assignable(source: TypeId, target: TypeId) -> PendingDiagnostic {
1247        PendingDiagnostic::error(
1248            codes::TYPE_NOT_ASSIGNABLE,
1249            vec![source.into(), target.into()],
1250        )
1251    }
1252
1253    /// Create a "Property X is missing" pending diagnostic.
1254    pub fn property_missing(prop_name: &str, source: TypeId, target: TypeId) -> PendingDiagnostic {
1255        PendingDiagnostic::error(
1256            codes::PROPERTY_MISSING,
1257            vec![prop_name.into(), source.into(), target.into()],
1258        )
1259    }
1260
1261    /// Create a "Property X does not exist" pending diagnostic.
1262    pub fn property_not_exist(prop_name: &str, type_id: TypeId) -> PendingDiagnostic {
1263        PendingDiagnostic::error(
1264            codes::PROPERTY_NOT_EXIST,
1265            vec![prop_name.into(), type_id.into()],
1266        )
1267    }
1268
1269    /// Create a "Cannot find name" pending diagnostic.
1270    pub fn cannot_find_name(name: &str) -> PendingDiagnostic {
1271        let code = cannot_find_name_code(name);
1272        PendingDiagnostic::error(code, vec![name.into()])
1273    }
1274
1275    /// Create a "Type is not callable" pending diagnostic.
1276    pub fn not_callable(type_id: TypeId) -> PendingDiagnostic {
1277        PendingDiagnostic::error(codes::NOT_CALLABLE, vec![type_id.into()])
1278    }
1279
1280    pub fn this_type_mismatch(expected_this: TypeId, actual_this: TypeId) -> PendingDiagnostic {
1281        PendingDiagnostic::error(
1282            codes::THIS_TYPE_MISMATCH,
1283            vec![actual_this.into(), expected_this.into()],
1284        )
1285    }
1286
1287    /// Create a "Cannot assign to readonly property" pending diagnostic.
1288    pub fn readonly_property(prop_name: &str) -> PendingDiagnostic {
1289        PendingDiagnostic::error(codes::READONLY_PROPERTY, vec![prop_name.into()])
1290    }
1291
1292    /// Create an "Excess property" pending diagnostic.
1293    pub fn excess_property(prop_name: &str, target: TypeId) -> PendingDiagnostic {
1294        PendingDiagnostic::error(
1295            codes::EXCESS_PROPERTY,
1296            vec![prop_name.into(), target.into()],
1297        )
1298    }
1299}
1300
1301// =============================================================================
1302// Spanned Diagnostic Builder
1303// =============================================================================
1304
1305/// A diagnostic builder that automatically attaches source spans.
1306///
1307/// This builder wraps `DiagnosticBuilder` and requires a file name and
1308/// position information for each diagnostic.
1309pub struct SpannedDiagnosticBuilder<'a> {
1310    builder: DiagnosticBuilder<'a>,
1311    file: Arc<str>,
1312}
1313
1314impl<'a> SpannedDiagnosticBuilder<'a> {
1315    pub fn new(interner: &'a dyn TypeDatabase, file: impl Into<Arc<str>>) -> Self {
1316        SpannedDiagnosticBuilder {
1317            builder: DiagnosticBuilder::new(interner),
1318            file: file.into(),
1319        }
1320    }
1321
1322    /// Create a spanned diagnostic builder with access to symbol names.
1323    ///
1324    /// This prevents "Ref(N)" fallback strings in diagnostic messages by
1325    /// resolving symbol references to their actual names.
1326    pub fn with_symbols(
1327        interner: &'a dyn TypeDatabase,
1328        symbol_arena: &'a tsz_binder::SymbolArena,
1329        file: impl Into<Arc<str>>,
1330    ) -> Self {
1331        SpannedDiagnosticBuilder {
1332            builder: DiagnosticBuilder::with_symbols(interner, symbol_arena),
1333            file: file.into(),
1334        }
1335    }
1336
1337    /// Add access to definition store for `DefId` name resolution.
1338    ///
1339    /// This prevents "Lazy(N)" fallback strings in diagnostic messages by
1340    /// resolving `DefIds` to their type names.
1341    pub fn with_def_store(mut self, def_store: &'a DefinitionStore) -> Self {
1342        self.builder = self.builder.with_def_store(def_store);
1343        self
1344    }
1345
1346    /// Create a span for this file.
1347    pub fn span(&self, start: u32, length: u32) -> SourceSpan {
1348        SourceSpan::new(std::sync::Arc::clone(&self.file), start, length)
1349    }
1350
1351    /// Create a "Type X is not assignable to type Y" diagnostic with span.
1352    pub fn type_not_assignable(
1353        &mut self,
1354        source: TypeId,
1355        target: TypeId,
1356        start: u32,
1357        length: u32,
1358    ) -> TypeDiagnostic {
1359        self.builder
1360            .type_not_assignable(source, target)
1361            .with_span(self.span(start, length))
1362    }
1363
1364    /// Create a "Property X is missing" diagnostic with span.
1365    pub fn property_missing(
1366        &mut self,
1367        prop_name: &str,
1368        source: TypeId,
1369        target: TypeId,
1370        start: u32,
1371        length: u32,
1372    ) -> TypeDiagnostic {
1373        self.builder
1374            .property_missing(prop_name, source, target)
1375            .with_span(self.span(start, length))
1376    }
1377
1378    /// Create a "Property X does not exist" diagnostic with span.
1379    pub fn property_not_exist(
1380        &mut self,
1381        prop_name: &str,
1382        type_id: TypeId,
1383        start: u32,
1384        length: u32,
1385    ) -> TypeDiagnostic {
1386        self.builder
1387            .property_not_exist(prop_name, type_id)
1388            .with_span(self.span(start, length))
1389    }
1390
1391    /// Create a "Property X does not exist on type Y. Did you mean Z?" diagnostic with span (TS2551).
1392    pub fn property_not_exist_did_you_mean(
1393        &mut self,
1394        prop_name: &str,
1395        type_id: TypeId,
1396        suggestion: &str,
1397        start: u32,
1398        length: u32,
1399    ) -> TypeDiagnostic {
1400        self.builder
1401            .property_not_exist_did_you_mean(prop_name, type_id, suggestion)
1402            .with_span(self.span(start, length))
1403    }
1404
1405    /// Create an "Argument not assignable" diagnostic with span.
1406    pub fn argument_not_assignable(
1407        &mut self,
1408        arg_type: TypeId,
1409        param_type: TypeId,
1410        start: u32,
1411        length: u32,
1412    ) -> TypeDiagnostic {
1413        self.builder
1414            .argument_not_assignable(arg_type, param_type)
1415            .with_span(self.span(start, length))
1416    }
1417
1418    /// Create a "Cannot find name" diagnostic with span.
1419    pub fn cannot_find_name(&mut self, name: &str, start: u32, length: u32) -> TypeDiagnostic {
1420        self.builder
1421            .cannot_find_name(name)
1422            .with_span(self.span(start, length))
1423    }
1424
1425    /// Create an "Expected N arguments" diagnostic with span.
1426    pub fn argument_count_mismatch(
1427        &mut self,
1428        expected: usize,
1429        got: usize,
1430        start: u32,
1431        length: u32,
1432    ) -> TypeDiagnostic {
1433        self.builder
1434            .argument_count_mismatch(expected, got)
1435            .with_span(self.span(start, length))
1436    }
1437
1438    /// Create a "Type is not callable" diagnostic with span.
1439    pub fn not_callable(&mut self, type_id: TypeId, start: u32, length: u32) -> TypeDiagnostic {
1440        self.builder
1441            .not_callable(type_id)
1442            .with_span(self.span(start, length))
1443    }
1444
1445    pub fn this_type_mismatch(
1446        &mut self,
1447        expected_this: TypeId,
1448        actual_this: TypeId,
1449        start: u32,
1450        length: u32,
1451    ) -> TypeDiagnostic {
1452        self.builder
1453            .this_type_mismatch(expected_this, actual_this)
1454            .with_span(self.span(start, length))
1455    }
1456
1457    /// Create an "Excess property" diagnostic with span.
1458    pub fn excess_property(
1459        &mut self,
1460        prop_name: &str,
1461        target: TypeId,
1462        start: u32,
1463        length: u32,
1464    ) -> TypeDiagnostic {
1465        self.builder
1466            .excess_property(prop_name, target)
1467            .with_span(self.span(start, length))
1468    }
1469
1470    /// Create a "Cannot assign to readonly property" diagnostic with span.
1471    pub fn readonly_property(
1472        &mut self,
1473        prop_name: &str,
1474        start: u32,
1475        length: u32,
1476    ) -> TypeDiagnostic {
1477        self.builder
1478            .readonly_property(prop_name)
1479            .with_span(self.span(start, length))
1480    }
1481
1482    /// Add a related location to an existing diagnostic.
1483    pub fn add_related(
1484        &self,
1485        diag: TypeDiagnostic,
1486        message: impl Into<String>,
1487        start: u32,
1488        length: u32,
1489    ) -> TypeDiagnostic {
1490        diag.with_related(self.span(start, length), message)
1491    }
1492}
1493
1494// =============================================================================
1495// Diagnostic Conversion
1496// =============================================================================
1497
1498/// Convert a solver `TypeDiagnostic` to a checker Diagnostic.
1499///
1500/// This allows the solver's diagnostic infrastructure to integrate
1501/// with the existing checker diagnostic system.
1502impl TypeDiagnostic {
1503    /// Convert to a `checker::Diagnostic`.
1504    ///
1505    /// Uses the provided `file_name` if no span is present.
1506    pub fn to_checker_diagnostic(&self, default_file: &str) -> tsz_common::diagnostics::Diagnostic {
1507        use tsz_common::diagnostics::{
1508            Diagnostic, DiagnosticCategory, DiagnosticRelatedInformation,
1509        };
1510
1511        let (file, start, length) = if let Some(ref span) = self.span {
1512            (span.file.to_string(), span.start, span.length)
1513        } else {
1514            (default_file.to_string(), 0, 0)
1515        };
1516
1517        let category = match self.severity {
1518            DiagnosticSeverity::Error => DiagnosticCategory::Error,
1519            DiagnosticSeverity::Warning => DiagnosticCategory::Warning,
1520            DiagnosticSeverity::Suggestion => DiagnosticCategory::Suggestion,
1521            DiagnosticSeverity::Message => DiagnosticCategory::Message,
1522        };
1523
1524        let related_information: Vec<DiagnosticRelatedInformation> = self
1525            .related
1526            .iter()
1527            .map(|rel| DiagnosticRelatedInformation {
1528                file: rel.span.file.to_string(),
1529                start: rel.span.start,
1530                length: rel.span.length,
1531                message_text: rel.message.clone(),
1532                category: DiagnosticCategory::Message,
1533                code: 0,
1534            })
1535            .collect();
1536
1537        Diagnostic {
1538            file,
1539            start,
1540            length,
1541            message_text: self.message.clone(),
1542            category,
1543            code: self.code,
1544            related_information,
1545        }
1546    }
1547}
1548
1549// =============================================================================
1550// Source Location Tracker
1551// =============================================================================
1552
1553/// Tracks source locations for AST nodes during type checking.
1554///
1555/// This struct provides a convenient way to associate type checking
1556/// operations with their source locations for diagnostic generation.
1557#[derive(Clone)]
1558pub struct SourceLocation {
1559    /// File name
1560    pub file: Arc<str>,
1561    /// Start position (byte offset)
1562    pub start: u32,
1563    /// End position (byte offset)
1564    pub end: u32,
1565}
1566
1567impl SourceLocation {
1568    pub fn new(file: impl Into<Arc<str>>, start: u32, end: u32) -> Self {
1569        Self {
1570            file: file.into(),
1571            start,
1572            end,
1573        }
1574    }
1575
1576    /// Get the length of this location.
1577    pub const fn length(&self) -> u32 {
1578        self.end.saturating_sub(self.start)
1579    }
1580
1581    /// Convert to a `SourceSpan`.
1582    pub fn to_span(&self) -> SourceSpan {
1583        SourceSpan::new(std::sync::Arc::clone(&self.file), self.start, self.length())
1584    }
1585}
1586
1587/// A diagnostic collector that accumulates diagnostics with source tracking.
1588pub struct DiagnosticCollector<'a> {
1589    interner: &'a dyn TypeDatabase,
1590    file: Arc<str>,
1591    diagnostics: Vec<TypeDiagnostic>,
1592}
1593
1594impl<'a> DiagnosticCollector<'a> {
1595    pub fn new(interner: &'a dyn TypeDatabase, file: impl Into<Arc<str>>) -> Self {
1596        DiagnosticCollector {
1597            interner,
1598            file: file.into(),
1599            diagnostics: Vec::new(),
1600        }
1601    }
1602
1603    /// Get the collected diagnostics.
1604    pub fn diagnostics(&self) -> &[TypeDiagnostic] {
1605        &self.diagnostics
1606    }
1607
1608    /// Take the collected diagnostics.
1609    pub fn take_diagnostics(&mut self) -> Vec<TypeDiagnostic> {
1610        std::mem::take(&mut self.diagnostics)
1611    }
1612
1613    /// Report a type not assignable error.
1614    pub fn type_not_assignable(&mut self, source: TypeId, target: TypeId, loc: &SourceLocation) {
1615        let mut builder = DiagnosticBuilder::new(self.interner);
1616        let diag = builder
1617            .type_not_assignable(source, target)
1618            .with_span(loc.to_span());
1619        self.diagnostics.push(diag);
1620    }
1621
1622    /// Report a property missing error.
1623    pub fn property_missing(
1624        &mut self,
1625        prop_name: &str,
1626        source: TypeId,
1627        target: TypeId,
1628        loc: &SourceLocation,
1629    ) {
1630        let mut builder = DiagnosticBuilder::new(self.interner);
1631        let diag = builder
1632            .property_missing(prop_name, source, target)
1633            .with_span(loc.to_span());
1634        self.diagnostics.push(diag);
1635    }
1636
1637    /// Report a property not exist error.
1638    pub fn property_not_exist(&mut self, prop_name: &str, type_id: TypeId, loc: &SourceLocation) {
1639        let mut builder = DiagnosticBuilder::new(self.interner);
1640        let diag = builder
1641            .property_not_exist(prop_name, type_id)
1642            .with_span(loc.to_span());
1643        self.diagnostics.push(diag);
1644    }
1645
1646    /// Report an argument not assignable error.
1647    pub fn argument_not_assignable(
1648        &mut self,
1649        arg_type: TypeId,
1650        param_type: TypeId,
1651        loc: &SourceLocation,
1652    ) {
1653        let mut builder = DiagnosticBuilder::new(self.interner);
1654        let diag = builder
1655            .argument_not_assignable(arg_type, param_type)
1656            .with_span(loc.to_span());
1657        self.diagnostics.push(diag);
1658    }
1659
1660    /// Report a cannot find name error.
1661    pub fn cannot_find_name(&mut self, name: &str, loc: &SourceLocation) {
1662        let mut builder = DiagnosticBuilder::new(self.interner);
1663        let diag = builder.cannot_find_name(name).with_span(loc.to_span());
1664        self.diagnostics.push(diag);
1665    }
1666
1667    /// Report an argument count mismatch error.
1668    pub fn argument_count_mismatch(&mut self, expected: usize, got: usize, loc: &SourceLocation) {
1669        let mut builder = DiagnosticBuilder::new(self.interner);
1670        let diag = builder
1671            .argument_count_mismatch(expected, got)
1672            .with_span(loc.to_span());
1673        self.diagnostics.push(diag);
1674    }
1675
1676    /// Convert all collected diagnostics to checker diagnostics.
1677    pub fn to_checker_diagnostics(&self) -> Vec<tsz_common::diagnostics::Diagnostic> {
1678        self.diagnostics
1679            .iter()
1680            .map(|d| d.to_checker_diagnostic(&self.file))
1681            .collect()
1682    }
1683}
1684
1685#[cfg(test)]
1686use crate::types::*;
1687
1688#[cfg(test)]
1689#[path = "../tests/diagnostics_tests.rs"]
1690mod tests;