Skip to main content

tsz_solver/diagnostics/
builders.rs

1//! Diagnostic builder types for constructing formatted error messages.
2//!
3//! This module contains the eagerly-rendered diagnostic builders that format
4//! human-readable error strings using `TypeFormatter`. These are consumed by
5//! the checker for user-facing output.
6//!
7//! - [`DiagnosticBuilder`]: Core builder that formats type names into messages
8//! - [`SpannedDiagnosticBuilder`]: Wraps `DiagnosticBuilder` with source spans
9//! - [`DiagnosticCollector`]: Accumulates diagnostics with source tracking
10//! - [`SourceLocation`]: Tracks source positions for AST nodes
11
12use crate::TypeDatabase;
13use crate::def::DefinitionStore;
14use crate::diagnostics::{DiagnosticSeverity, SourceSpan, TypeDiagnostic, codes};
15use crate::format::TypeFormatter;
16use crate::types::TypeId;
17use std::sync::Arc;
18
19// =============================================================================
20// Diagnostic Builder
21// =============================================================================
22
23/// Builder for creating type error diagnostics.
24pub struct DiagnosticBuilder<'a> {
25    formatter: TypeFormatter<'a>,
26}
27
28impl<'a> DiagnosticBuilder<'a> {
29    pub fn new(interner: &'a dyn TypeDatabase) -> Self {
30        DiagnosticBuilder {
31            formatter: TypeFormatter::new(interner),
32        }
33    }
34
35    /// Create a diagnostic builder with access to symbol names.
36    ///
37    /// This prevents "Ref(N)" fallback strings in diagnostic messages by
38    /// resolving symbol references to their actual names.
39    pub fn with_symbols(
40        interner: &'a dyn TypeDatabase,
41        symbol_arena: &'a tsz_binder::SymbolArena,
42    ) -> Self {
43        DiagnosticBuilder {
44            formatter: TypeFormatter::with_symbols(interner, symbol_arena),
45        }
46    }
47
48    /// Create a diagnostic builder with access to definition store.
49    ///
50    /// This prevents "Lazy(N)" fallback strings in diagnostic messages by
51    /// resolving `DefIds` to their type names.
52    pub fn with_def_store(mut self, def_store: &'a DefinitionStore) -> Self {
53        self.formatter = self.formatter.with_def_store(def_store);
54        self
55    }
56
57    /// Create a "Type X is not assignable to type Y" diagnostic.
58    pub fn type_not_assignable(&mut self, source: TypeId, target: TypeId) -> TypeDiagnostic {
59        let source_str = self.formatter.format(source);
60        let target_str = self.formatter.format(target);
61        TypeDiagnostic::error(
62            format!("Type '{source_str}' is not assignable to type '{target_str}'."),
63            codes::TYPE_NOT_ASSIGNABLE,
64        )
65    }
66
67    /// Create a "Property X is missing in type Y" diagnostic.
68    pub fn property_missing(
69        &mut self,
70        prop_name: &str,
71        source: TypeId,
72        target: TypeId,
73    ) -> TypeDiagnostic {
74        let source_str = self.formatter.format(source);
75        let target_str = self.formatter.format(target);
76        TypeDiagnostic::error(
77            format!(
78                "Property '{prop_name}' is missing in type '{source_str}' but required in type '{target_str}'."
79            ),
80            codes::PROPERTY_MISSING,
81        )
82    }
83
84    /// Create a "Property X does not exist on type Y" diagnostic.
85    pub fn property_not_exist(&mut self, prop_name: &str, type_id: TypeId) -> TypeDiagnostic {
86        let type_str = self.formatter.format(type_id);
87        TypeDiagnostic::error(
88            format!("Property '{prop_name}' does not exist on type '{type_str}'."),
89            codes::PROPERTY_NOT_EXIST,
90        )
91    }
92
93    /// Create a "Property X does not exist on type Y. Did you mean Z?" diagnostic (TS2551).
94    pub fn property_not_exist_did_you_mean(
95        &mut self,
96        prop_name: &str,
97        type_id: TypeId,
98        suggestion: &str,
99    ) -> TypeDiagnostic {
100        let type_str = self.formatter.format(type_id);
101        TypeDiagnostic::error(
102            format!(
103                "Property '{prop_name}' does not exist on type '{type_str}'. Did you mean '{suggestion}'?"
104            ),
105            codes::PROPERTY_NOT_EXIST_DID_YOU_MEAN,
106        )
107    }
108
109    /// Create an "Argument not assignable" diagnostic.
110    pub fn argument_not_assignable(
111        &mut self,
112        arg_type: TypeId,
113        param_type: TypeId,
114    ) -> TypeDiagnostic {
115        let arg_str = self.formatter.format(arg_type);
116        let param_str = self.formatter.format(param_type);
117        TypeDiagnostic::error(
118            format!(
119                "Argument of type '{arg_str}' is not assignable to parameter of type '{param_str}'."
120            ),
121            codes::ARG_NOT_ASSIGNABLE,
122        )
123    }
124
125    /// Create a "Cannot find name" diagnostic.
126    pub fn cannot_find_name(&mut self, name: &str) -> TypeDiagnostic {
127        // Skip TS2304 for identifiers that are clearly not valid names.
128        // These are likely parse errors (e.g., ",", ";", "(") that were
129        // added to the AST for error recovery. The parse error should have
130        // already been emitted (e.g., TS1136 "Property assignment expected").
131        let is_obviously_invalid = name.len() == 1
132            && matches!(
133                name.chars().next(),
134                Some(
135                    ',' | ';'
136                        | ':'
137                        | '('
138                        | ')'
139                        | '['
140                        | ']'
141                        | '{'
142                        | '}'
143                        | '+'
144                        | '-'
145                        | '*'
146                        | '/'
147                        | '%'
148                        | '&'
149                        | '|'
150                        | '^'
151                        | '!'
152                        | '~'
153                        | '<'
154                        | '>'
155                        | '='
156                        | '.'
157                )
158            );
159
160        if is_obviously_invalid {
161            // Return a dummy diagnostic with empty message that will be ignored
162            return TypeDiagnostic::error("", 0);
163        }
164
165        let code = crate::diagnostics::cannot_find_name_code(name);
166        TypeDiagnostic::error(format!("Cannot find name '{name}'."), code)
167    }
168
169    /// Create a "Type X is not callable" diagnostic.
170    pub fn not_callable(&mut self, type_id: TypeId) -> TypeDiagnostic {
171        let type_str = self.formatter.format(type_id);
172        TypeDiagnostic::error(
173            format!("Type '{type_str}' has no call signatures."),
174            codes::NOT_CALLABLE,
175        )
176    }
177
178    pub fn this_type_mismatch(
179        &mut self,
180        expected_this: TypeId,
181        actual_this: TypeId,
182    ) -> TypeDiagnostic {
183        let expected_str = self.formatter.format(expected_this);
184        let actual_str = self.formatter.format(actual_this);
185        TypeDiagnostic::error(
186            format!(
187                "The 'this' context of type '{actual_str}' is not assignable to method's 'this' of type '{expected_str}'."
188            ),
189            codes::THIS_TYPE_MISMATCH,
190        )
191    }
192
193    /// Create an "Expected N arguments but got M" diagnostic.
194    pub fn argument_count_mismatch(&mut self, expected: usize, got: usize) -> TypeDiagnostic {
195        TypeDiagnostic::error(
196            format!("Expected {expected} arguments, but got {got}."),
197            codes::ARG_COUNT_MISMATCH,
198        )
199    }
200
201    /// Create a "Cannot assign to readonly property" diagnostic.
202    pub fn readonly_property(&mut self, prop_name: &str) -> TypeDiagnostic {
203        TypeDiagnostic::error(
204            format!("Cannot assign to '{prop_name}' because it is a read-only property."),
205            codes::READONLY_PROPERTY,
206        )
207    }
208
209    /// Create an "Excess property" diagnostic.
210    pub fn excess_property(&mut self, prop_name: &str, target: TypeId) -> TypeDiagnostic {
211        let target_str = self.formatter.format(target);
212        TypeDiagnostic::error(
213            format!(
214                "Object literal may only specify known properties, and '{prop_name}' does not exist in type '{target_str}'."
215            ),
216            codes::EXCESS_PROPERTY,
217        )
218    }
219
220    // =========================================================================
221    // Implicit Any Diagnostics (TS7006, TS7008, TS7010, TS7011)
222    // =========================================================================
223
224    /// Create a "Parameter implicitly has an 'any' type" diagnostic (TS7006).
225    pub fn implicit_any_parameter(&mut self, param_name: &str) -> TypeDiagnostic {
226        TypeDiagnostic::error(
227            format!("Parameter '{param_name}' implicitly has an 'any' type."),
228            codes::IMPLICIT_ANY_PARAMETER,
229        )
230    }
231
232    /// Create a "Parameter implicitly has a specific type" diagnostic (TS7006 variant).
233    pub fn implicit_any_parameter_with_type(
234        &mut self,
235        param_name: &str,
236        implicit_type: TypeId,
237    ) -> TypeDiagnostic {
238        let type_str = self.formatter.format(implicit_type);
239        TypeDiagnostic::error(
240            format!("Parameter '{param_name}' implicitly has an '{type_str}' type."),
241            codes::IMPLICIT_ANY_PARAMETER,
242        )
243    }
244
245    /// Create a "Member implicitly has an 'any' type" diagnostic (TS7008).
246    pub fn implicit_any_member(&mut self, member_name: &str) -> TypeDiagnostic {
247        TypeDiagnostic::error(
248            format!("Member '{member_name}' implicitly has an 'any' type."),
249            codes::IMPLICIT_ANY_MEMBER,
250        )
251    }
252
253    /// Create a "Variable implicitly has an 'any' type" diagnostic (TS7005).
254    pub fn implicit_any_variable(&mut self, var_name: &str, var_type: TypeId) -> TypeDiagnostic {
255        let type_str = self.formatter.format(var_type);
256        TypeDiagnostic::error(
257            format!("Variable '{var_name}' implicitly has an '{type_str}' type."),
258            codes::IMPLICIT_ANY,
259        )
260    }
261
262    /// Create an "implicitly has an 'any' return type" diagnostic (TS7010).
263    pub fn implicit_any_return(&mut self, func_name: &str, return_type: TypeId) -> TypeDiagnostic {
264        let type_str = self.formatter.format(return_type);
265        TypeDiagnostic::error(
266            format!(
267                "'{func_name}', which lacks return-type annotation, implicitly has an '{type_str}' return type."
268            ),
269            codes::IMPLICIT_ANY_RETURN,
270        )
271    }
272
273    /// Create a "Function expression implicitly has an 'any' return type" diagnostic (TS7011).
274    pub fn implicit_any_return_function_expression(
275        &mut self,
276        return_type: TypeId,
277    ) -> TypeDiagnostic {
278        let type_str = self.formatter.format(return_type);
279        TypeDiagnostic::error(
280            format!(
281                "Function expression, which lacks return-type annotation, implicitly has an '{type_str}' return type."
282            ),
283            codes::IMPLICIT_ANY_RETURN_FUNCTION_EXPRESSION,
284        )
285    }
286}
287
288// =============================================================================
289// Spanned Diagnostic Builder
290// =============================================================================
291
292/// A diagnostic builder that automatically attaches source spans.
293///
294/// This builder wraps `DiagnosticBuilder` and requires a file name and
295/// position information for each diagnostic.
296pub struct SpannedDiagnosticBuilder<'a> {
297    builder: DiagnosticBuilder<'a>,
298    file: Arc<str>,
299}
300
301impl<'a> SpannedDiagnosticBuilder<'a> {
302    pub fn new(interner: &'a dyn TypeDatabase, file: impl Into<Arc<str>>) -> Self {
303        SpannedDiagnosticBuilder {
304            builder: DiagnosticBuilder::new(interner),
305            file: file.into(),
306        }
307    }
308
309    /// Create a spanned diagnostic builder with access to symbol names.
310    pub fn with_symbols(
311        interner: &'a dyn TypeDatabase,
312        symbol_arena: &'a tsz_binder::SymbolArena,
313        file: impl Into<Arc<str>>,
314    ) -> Self {
315        SpannedDiagnosticBuilder {
316            builder: DiagnosticBuilder::with_symbols(interner, symbol_arena),
317            file: file.into(),
318        }
319    }
320
321    /// Add access to definition store for `DefId` name resolution.
322    pub fn with_def_store(mut self, def_store: &'a DefinitionStore) -> Self {
323        self.builder = self.builder.with_def_store(def_store);
324        self
325    }
326
327    /// Create a span for this file.
328    pub fn span(&self, start: u32, length: u32) -> SourceSpan {
329        SourceSpan::new(std::sync::Arc::clone(&self.file), start, length)
330    }
331
332    /// Create a "Type X is not assignable to type Y" diagnostic with span.
333    pub fn type_not_assignable(
334        &mut self,
335        source: TypeId,
336        target: TypeId,
337        start: u32,
338        length: u32,
339    ) -> TypeDiagnostic {
340        self.builder
341            .type_not_assignable(source, target)
342            .with_span(self.span(start, length))
343    }
344
345    /// Create a "Property X is missing" diagnostic with span.
346    pub fn property_missing(
347        &mut self,
348        prop_name: &str,
349        source: TypeId,
350        target: TypeId,
351        start: u32,
352        length: u32,
353    ) -> TypeDiagnostic {
354        self.builder
355            .property_missing(prop_name, source, target)
356            .with_span(self.span(start, length))
357    }
358
359    /// Create a "Property X does not exist" diagnostic with span.
360    pub fn property_not_exist(
361        &mut self,
362        prop_name: &str,
363        type_id: TypeId,
364        start: u32,
365        length: u32,
366    ) -> TypeDiagnostic {
367        self.builder
368            .property_not_exist(prop_name, type_id)
369            .with_span(self.span(start, length))
370    }
371
372    /// Create a "Property X does not exist on type Y. Did you mean Z?" diagnostic with span (TS2551).
373    pub fn property_not_exist_did_you_mean(
374        &mut self,
375        prop_name: &str,
376        type_id: TypeId,
377        suggestion: &str,
378        start: u32,
379        length: u32,
380    ) -> TypeDiagnostic {
381        self.builder
382            .property_not_exist_did_you_mean(prop_name, type_id, suggestion)
383            .with_span(self.span(start, length))
384    }
385
386    /// Create an "Argument not assignable" diagnostic with span.
387    pub fn argument_not_assignable(
388        &mut self,
389        arg_type: TypeId,
390        param_type: TypeId,
391        start: u32,
392        length: u32,
393    ) -> TypeDiagnostic {
394        self.builder
395            .argument_not_assignable(arg_type, param_type)
396            .with_span(self.span(start, length))
397    }
398
399    /// Create a "Cannot find name" diagnostic with span.
400    pub fn cannot_find_name(&mut self, name: &str, start: u32, length: u32) -> TypeDiagnostic {
401        self.builder
402            .cannot_find_name(name)
403            .with_span(self.span(start, length))
404    }
405
406    /// Create an "Expected N arguments" diagnostic with span.
407    pub fn argument_count_mismatch(
408        &mut self,
409        expected: usize,
410        got: usize,
411        start: u32,
412        length: u32,
413    ) -> TypeDiagnostic {
414        self.builder
415            .argument_count_mismatch(expected, got)
416            .with_span(self.span(start, length))
417    }
418
419    /// Create a "Type is not callable" diagnostic with span.
420    pub fn not_callable(&mut self, type_id: TypeId, start: u32, length: u32) -> TypeDiagnostic {
421        self.builder
422            .not_callable(type_id)
423            .with_span(self.span(start, length))
424    }
425
426    pub fn this_type_mismatch(
427        &mut self,
428        expected_this: TypeId,
429        actual_this: TypeId,
430        start: u32,
431        length: u32,
432    ) -> TypeDiagnostic {
433        self.builder
434            .this_type_mismatch(expected_this, actual_this)
435            .with_span(self.span(start, length))
436    }
437
438    /// Create an "Excess property" diagnostic with span.
439    pub fn excess_property(
440        &mut self,
441        prop_name: &str,
442        target: TypeId,
443        start: u32,
444        length: u32,
445    ) -> TypeDiagnostic {
446        self.builder
447            .excess_property(prop_name, target)
448            .with_span(self.span(start, length))
449    }
450
451    /// Create a "Cannot assign to readonly property" diagnostic with span.
452    pub fn readonly_property(
453        &mut self,
454        prop_name: &str,
455        start: u32,
456        length: u32,
457    ) -> TypeDiagnostic {
458        self.builder
459            .readonly_property(prop_name)
460            .with_span(self.span(start, length))
461    }
462
463    /// Add a related location to an existing diagnostic.
464    pub fn add_related(
465        &self,
466        diag: TypeDiagnostic,
467        message: impl Into<String>,
468        start: u32,
469        length: u32,
470    ) -> TypeDiagnostic {
471        diag.with_related(self.span(start, length), message)
472    }
473}
474
475// =============================================================================
476// Diagnostic Conversion
477// =============================================================================
478
479/// Convert a solver `TypeDiagnostic` to a checker Diagnostic.
480///
481/// This allows the solver's diagnostic infrastructure to integrate
482/// with the existing checker diagnostic system.
483impl TypeDiagnostic {
484    /// Convert to a `checker::Diagnostic`.
485    ///
486    /// Uses the provided `file_name` if no span is present.
487    pub fn to_checker_diagnostic(&self, default_file: &str) -> tsz_common::diagnostics::Diagnostic {
488        use tsz_common::diagnostics::{
489            Diagnostic, DiagnosticCategory, DiagnosticRelatedInformation,
490        };
491
492        let (file, start, length) = if let Some(ref span) = self.span {
493            (span.file.to_string(), span.start, span.length)
494        } else {
495            (default_file.to_string(), 0, 0)
496        };
497
498        let category = match self.severity {
499            DiagnosticSeverity::Error => DiagnosticCategory::Error,
500            DiagnosticSeverity::Warning => DiagnosticCategory::Warning,
501            DiagnosticSeverity::Suggestion => DiagnosticCategory::Suggestion,
502            DiagnosticSeverity::Message => DiagnosticCategory::Message,
503        };
504
505        let related_information: Vec<DiagnosticRelatedInformation> = self
506            .related
507            .iter()
508            .map(|rel| DiagnosticRelatedInformation {
509                file: rel.span.file.to_string(),
510                start: rel.span.start,
511                length: rel.span.length,
512                message_text: rel.message.clone(),
513                category: DiagnosticCategory::Message,
514                code: 0,
515            })
516            .collect();
517
518        Diagnostic {
519            file,
520            start,
521            length,
522            message_text: self.message.clone(),
523            category,
524            code: self.code,
525            related_information,
526        }
527    }
528}
529
530// =============================================================================
531// Source Location Tracker
532// =============================================================================
533
534/// Tracks source locations for AST nodes during type checking.
535///
536/// This struct provides a convenient way to associate type checking
537/// operations with their source locations for diagnostic generation.
538#[derive(Clone)]
539pub struct SourceLocation {
540    /// File name
541    pub file: Arc<str>,
542    /// Start position (byte offset)
543    pub start: u32,
544    /// End position (byte offset)
545    pub end: u32,
546}
547
548impl SourceLocation {
549    pub fn new(file: impl Into<Arc<str>>, start: u32, end: u32) -> Self {
550        Self {
551            file: file.into(),
552            start,
553            end,
554        }
555    }
556
557    /// Get the length of this location.
558    pub const fn length(&self) -> u32 {
559        self.end.saturating_sub(self.start)
560    }
561
562    /// Convert to a `SourceSpan`.
563    pub fn to_span(&self) -> SourceSpan {
564        SourceSpan::new(std::sync::Arc::clone(&self.file), self.start, self.length())
565    }
566}
567
568/// A diagnostic collector that accumulates diagnostics with source tracking.
569pub struct DiagnosticCollector<'a> {
570    interner: &'a dyn TypeDatabase,
571    file: Arc<str>,
572    diagnostics: Vec<TypeDiagnostic>,
573}
574
575impl<'a> DiagnosticCollector<'a> {
576    pub fn new(interner: &'a dyn TypeDatabase, file: impl Into<Arc<str>>) -> Self {
577        DiagnosticCollector {
578            interner,
579            file: file.into(),
580            diagnostics: Vec::new(),
581        }
582    }
583
584    /// Get the collected diagnostics.
585    pub fn diagnostics(&self) -> &[TypeDiagnostic] {
586        &self.diagnostics
587    }
588
589    /// Take the collected diagnostics.
590    pub fn take_diagnostics(&mut self) -> Vec<TypeDiagnostic> {
591        std::mem::take(&mut self.diagnostics)
592    }
593
594    /// Report a type not assignable error.
595    pub fn type_not_assignable(&mut self, source: TypeId, target: TypeId, loc: &SourceLocation) {
596        let mut builder = DiagnosticBuilder::new(self.interner);
597        let diag = builder
598            .type_not_assignable(source, target)
599            .with_span(loc.to_span());
600        self.diagnostics.push(diag);
601    }
602
603    /// Report a property missing error.
604    pub fn property_missing(
605        &mut self,
606        prop_name: &str,
607        source: TypeId,
608        target: TypeId,
609        loc: &SourceLocation,
610    ) {
611        let mut builder = DiagnosticBuilder::new(self.interner);
612        let diag = builder
613            .property_missing(prop_name, source, target)
614            .with_span(loc.to_span());
615        self.diagnostics.push(diag);
616    }
617
618    /// Report a property not exist error.
619    pub fn property_not_exist(&mut self, prop_name: &str, type_id: TypeId, loc: &SourceLocation) {
620        let mut builder = DiagnosticBuilder::new(self.interner);
621        let diag = builder
622            .property_not_exist(prop_name, type_id)
623            .with_span(loc.to_span());
624        self.diagnostics.push(diag);
625    }
626
627    /// Report an argument not assignable error.
628    pub fn argument_not_assignable(
629        &mut self,
630        arg_type: TypeId,
631        param_type: TypeId,
632        loc: &SourceLocation,
633    ) {
634        let mut builder = DiagnosticBuilder::new(self.interner);
635        let diag = builder
636            .argument_not_assignable(arg_type, param_type)
637            .with_span(loc.to_span());
638        self.diagnostics.push(diag);
639    }
640
641    /// Report a cannot find name error.
642    pub fn cannot_find_name(&mut self, name: &str, loc: &SourceLocation) {
643        let mut builder = DiagnosticBuilder::new(self.interner);
644        let diag = builder.cannot_find_name(name).with_span(loc.to_span());
645        self.diagnostics.push(diag);
646    }
647
648    /// Report an argument count mismatch error.
649    pub fn argument_count_mismatch(&mut self, expected: usize, got: usize, loc: &SourceLocation) {
650        let mut builder = DiagnosticBuilder::new(self.interner);
651        let diag = builder
652            .argument_count_mismatch(expected, got)
653            .with_span(loc.to_span());
654        self.diagnostics.push(diag);
655    }
656
657    /// Convert all collected diagnostics to checker diagnostics.
658    pub fn to_checker_diagnostics(&self) -> Vec<tsz_common::diagnostics::Diagnostic> {
659        self.diagnostics
660            .iter()
661            .map(|d| d.to_checker_diagnostic(&self.file))
662            .collect()
663    }
664}