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