Skip to main content

tsz_solver/
format.rs

1//! Type formatting for the solver.
2//! Centralizes logic for converting `TypeIds` and `TypeDatas` to human-readable strings.
3
4use crate::TypeDatabase;
5use crate::def::DefinitionStore;
6use crate::diagnostics::{
7    DiagnosticArg, PendingDiagnostic, RelatedInformation, SourceSpan, TypeDiagnostic,
8    get_message_template,
9};
10use crate::types::{
11    CallSignature, CallableShape, ConditionalType, FunctionShape, IntrinsicKind, LiteralValue,
12    MappedType, ObjectShape, ParamInfo, PropertyInfo, StringIntrinsicKind, TemplateSpan,
13    TupleElement, TypeData, TypeId, TypeParamInfo,
14};
15use rustc_hash::FxHashMap;
16use std::sync::Arc;
17use tracing::trace;
18use tsz_binder::SymbolId;
19use tsz_common::interner::Atom;
20
21/// Context for generating type strings.
22pub struct TypeFormatter<'a> {
23    interner: &'a dyn TypeDatabase,
24    /// Symbol arena for looking up symbol names (optional)
25    symbol_arena: Option<&'a tsz_binder::SymbolArena>,
26    /// Definition store for looking up `DefId` names (optional)
27    def_store: Option<&'a DefinitionStore>,
28    /// Maximum depth for nested type printing
29    max_depth: u32,
30    /// Maximum number of union members to display before truncating
31    max_union_members: usize,
32    /// Current depth
33    current_depth: u32,
34    atom_cache: FxHashMap<Atom, Arc<str>>,
35}
36
37impl<'a> TypeFormatter<'a> {
38    pub fn new(interner: &'a dyn TypeDatabase) -> Self {
39        TypeFormatter {
40            interner,
41            symbol_arena: None,
42            def_store: None,
43            max_depth: 5,
44            max_union_members: 5,
45            current_depth: 0,
46            atom_cache: FxHashMap::default(),
47        }
48    }
49
50    /// Create a formatter with access to symbol names.
51    pub fn with_symbols(
52        interner: &'a dyn TypeDatabase,
53        symbol_arena: &'a tsz_binder::SymbolArena,
54    ) -> Self {
55        TypeFormatter {
56            interner,
57            symbol_arena: Some(symbol_arena),
58            def_store: None,
59            max_depth: 5,
60            max_union_members: 5,
61            current_depth: 0,
62            atom_cache: FxHashMap::default(),
63        }
64    }
65
66    /// Add access to definition store for `DefId` name resolution.
67    pub const fn with_def_store(mut self, def_store: &'a DefinitionStore) -> Self {
68        self.def_store = Some(def_store);
69        self
70    }
71
72    pub const fn with_limits(mut self, max_depth: u32, max_union_members: usize) -> Self {
73        self.max_depth = max_depth;
74        self.max_union_members = max_union_members;
75        self
76    }
77
78    fn atom(&mut self, atom: Atom) -> Arc<str> {
79        if let Some(value) = self.atom_cache.get(&atom) {
80            return std::sync::Arc::clone(value);
81        }
82        let resolved = self.interner.resolve_atom_ref(atom);
83        self.atom_cache
84            .insert(atom, std::sync::Arc::clone(&resolved));
85        resolved
86    }
87
88    /// Render a pending diagnostic to a complete diagnostic with formatted message.
89    ///
90    /// This is where the lazy evaluation happens - we format types to strings
91    /// only when the diagnostic is actually going to be displayed.
92    pub fn render(&mut self, pending: &PendingDiagnostic) -> TypeDiagnostic {
93        let template = get_message_template(pending.code);
94        let message = self.render_template(template, &pending.args);
95
96        let mut diag = TypeDiagnostic {
97            message,
98            code: pending.code,
99            severity: pending.severity,
100            span: pending.span.clone(),
101            related: Vec::new(),
102        };
103
104        // Render related diagnostics, falling back to the primary span.
105        let fallback_span = pending
106            .span
107            .clone()
108            .unwrap_or_else(|| SourceSpan::new("<unknown>", 0, 0));
109        for related in &pending.related {
110            let related_msg =
111                self.render_template(get_message_template(related.code), &related.args);
112            let span = related
113                .span
114                .clone()
115                .unwrap_or_else(|| fallback_span.clone());
116            diag.related.push(RelatedInformation {
117                span,
118                message: related_msg,
119            });
120        }
121
122        diag
123    }
124
125    /// Render a message template with arguments.
126    fn render_template(&mut self, template: &str, args: &[DiagnosticArg]) -> String {
127        let mut result = template.to_string();
128
129        for (i, arg) in args.iter().enumerate() {
130            let placeholder = format!("{{{i}}}");
131            if !template.contains(&placeholder) {
132                continue;
133            }
134            let replacement = match arg {
135                DiagnosticArg::Type(type_id) => self.format(*type_id),
136                DiagnosticArg::Symbol(sym_id) => {
137                    if let Some(name) = self.format_symbol_name(*sym_id) {
138                        name
139                    } else {
140                        format!("Symbol({})", sym_id.0)
141                    }
142                }
143                DiagnosticArg::Atom(atom) => self.atom(*atom).to_string(),
144                DiagnosticArg::String(s) => s.to_string(),
145                DiagnosticArg::Number(n) => n.to_string(),
146            };
147            result = result.replace(&placeholder, &replacement);
148        }
149
150        result
151    }
152
153    /// Format a type as a human-readable string.
154    pub fn format(&mut self, type_id: TypeId) -> String {
155        if self.current_depth >= self.max_depth {
156            return "...".to_string();
157        }
158
159        // Handle intrinsic types
160        match type_id {
161            TypeId::NEVER => return "never".to_string(),
162            TypeId::UNKNOWN => return "unknown".to_string(),
163            TypeId::ANY => return "any".to_string(),
164            TypeId::VOID => return "void".to_string(),
165            TypeId::UNDEFINED => return "undefined".to_string(),
166            TypeId::NULL => return "null".to_string(),
167            TypeId::BOOLEAN => return "boolean".to_string(),
168            TypeId::NUMBER => return "number".to_string(),
169            TypeId::STRING => return "string".to_string(),
170            TypeId::BIGINT => return "bigint".to_string(),
171            TypeId::SYMBOL => return "symbol".to_string(),
172            TypeId::OBJECT => return "object".to_string(),
173            TypeId::FUNCTION => return "Function".to_string(),
174            TypeId::ERROR => return "error".to_string(),
175            _ => {}
176        }
177
178        let key = match self.interner.lookup(type_id) {
179            Some(k) => k,
180            None => return format!("Type({})", type_id.0),
181        };
182
183        self.current_depth += 1;
184        let result = self.format_key(&key);
185        self.current_depth -= 1;
186        result
187    }
188
189    fn format_key(&mut self, key: &TypeData) -> String {
190        match key {
191            TypeData::Intrinsic(kind) => self.format_intrinsic(*kind),
192            TypeData::Literal(lit) => self.format_literal(lit),
193            TypeData::Object(shape_id) => {
194                let shape = self.interner.object_shape(*shape_id);
195                if let Some(name) = self.resolve_object_shape_name(&shape) {
196                    return name;
197                }
198                self.format_object(shape.properties.as_slice())
199            }
200            TypeData::ObjectWithIndex(shape_id) => {
201                let shape = self.interner.object_shape(*shape_id);
202                if let Some(name) = self.resolve_object_shape_name(&shape) {
203                    return name;
204                }
205                self.format_object_with_index(shape.as_ref())
206            }
207            TypeData::Union(members) => {
208                let members = self.interner.type_list(*members);
209                self.format_union(members.as_ref())
210            }
211            TypeData::Intersection(members) => {
212                let members = self.interner.type_list(*members);
213                self.format_intersection(members.as_ref())
214            }
215            TypeData::Array(elem) => {
216                let elem_formatted = self.format(*elem);
217                let needs_parens = matches!(
218                    self.interner.lookup(*elem),
219                    Some(
220                        TypeData::Union(_)
221                            | TypeData::Intersection(_)
222                            | TypeData::Function(_)
223                            | TypeData::Callable(_)
224                    )
225                );
226                if needs_parens {
227                    format!("({elem_formatted})[]")
228                } else {
229                    format!("{elem_formatted}[]")
230                }
231            }
232            TypeData::Tuple(elements) => {
233                let elements = self.interner.tuple_list(*elements);
234                self.format_tuple(elements.as_ref())
235            }
236            TypeData::Function(shape_id) => {
237                let shape = self.interner.function_shape(*shape_id);
238                self.format_function(shape.as_ref())
239            }
240            TypeData::Callable(shape_id) => {
241                let shape = self.interner.callable_shape(*shape_id);
242                self.format_callable(shape.as_ref())
243            }
244            TypeData::TypeParameter(info) => self.atom(info.name).to_string(),
245            TypeData::Lazy(def_id) => self.format_def_id(*def_id, "Lazy"),
246            TypeData::Recursive(idx) => {
247                format!("Recursive({idx})")
248            }
249            TypeData::BoundParameter(idx) => {
250                format!("BoundParameter({idx})")
251            }
252            TypeData::Application(app) => {
253                let app = self.interner.type_application(*app);
254                let base_key = self.interner.lookup(app.base);
255
256                trace!(
257                    base_type_id = %app.base.0,
258                    ?base_key,
259                    args_count = app.args.len(),
260                    "Formatting Application"
261                );
262
263                // Special handling for Application(Lazy(def_id), args)
264                // Format as "TypeName<Args>" instead of "Lazy(def_id)<Args>"
265                let base_str = if let Some(TypeData::Lazy(def_id)) = base_key {
266                    let name = self.format_def_id(def_id, "Lazy");
267                    trace!(
268                        def_id = %def_id.0,
269                        name = %name,
270                        "Application base resolved from DefId"
271                    );
272                    name
273                } else {
274                    let formatted = self.format(app.base);
275                    trace!(
276                        base_formatted = %formatted,
277                        "Application base formatted (not Lazy)"
278                    );
279                    formatted
280                };
281
282                let args: Vec<String> = app.args.iter().map(|&arg| self.format(arg)).collect();
283                let result = format!("{}<{}>", base_str, args.join(", "));
284                trace!(result = %result, "Application formatted");
285                result
286            }
287            TypeData::Conditional(cond_id) => {
288                let cond = self.interner.conditional_type(*cond_id);
289                self.format_conditional(cond.as_ref())
290            }
291            TypeData::Mapped(mapped_id) => {
292                let mapped = self.interner.mapped_type(*mapped_id);
293                self.format_mapped(mapped.as_ref())
294            }
295            TypeData::IndexAccess(obj, idx) => {
296                format!("{}[{}]", self.format(*obj), self.format(*idx))
297            }
298            TypeData::TemplateLiteral(spans) => {
299                let spans = self.interner.template_list(*spans);
300                self.format_template_literal(spans.as_ref())
301            }
302            TypeData::TypeQuery(sym) => {
303                let name = if let Some(name) = self.format_symbol_name(SymbolId(sym.0)) {
304                    name
305                } else {
306                    format!("Ref({})", sym.0)
307                };
308                format!("typeof {name}")
309            }
310            TypeData::KeyOf(operand) => format!("keyof {}", self.format(*operand)),
311            TypeData::ReadonlyType(inner) => format!("readonly {}", self.format(*inner)),
312            TypeData::NoInfer(inner) => format!("NoInfer<{}>", self.format(*inner)),
313            TypeData::UniqueSymbol(sym) => {
314                let name = if let Some(name) = self.format_symbol_name(SymbolId(sym.0)) {
315                    name
316                } else {
317                    format!("symbol({})", sym.0)
318                };
319                format!("unique symbol {name}")
320            }
321            TypeData::Infer(info) => format!("infer {}", self.atom(info.name)),
322            TypeData::ThisType => "this".to_string(),
323            TypeData::StringIntrinsic { kind, type_arg } => {
324                let kind_name = match kind {
325                    StringIntrinsicKind::Uppercase => "Uppercase",
326                    StringIntrinsicKind::Lowercase => "Lowercase",
327                    StringIntrinsicKind::Capitalize => "Capitalize",
328                    StringIntrinsicKind::Uncapitalize => "Uncapitalize",
329                };
330                format!("{}<{}>", kind_name, self.format(*type_arg))
331            }
332            TypeData::Enum(def_id, _member_type) => self.format_def_id(*def_id, "Enum"),
333            TypeData::ModuleNamespace(sym) => {
334                let name = if let Some(name) = self.format_symbol_name(SymbolId(sym.0)) {
335                    name
336                } else {
337                    format!("module({})", sym.0)
338                };
339                format!("typeof import(\"{name}\")")
340            }
341            TypeData::Error => "error".to_string(),
342        }
343    }
344
345    fn format_intrinsic(&self, kind: IntrinsicKind) -> String {
346        match kind {
347            IntrinsicKind::Any => "any",
348            IntrinsicKind::Unknown => "unknown",
349            IntrinsicKind::Never => "never",
350            IntrinsicKind::Void => "void",
351            IntrinsicKind::Null => "null",
352            IntrinsicKind::Undefined => "undefined",
353            IntrinsicKind::Boolean => "boolean",
354            IntrinsicKind::Number => "number",
355            IntrinsicKind::String => "string",
356            IntrinsicKind::Bigint => "bigint",
357            IntrinsicKind::Symbol => "symbol",
358            IntrinsicKind::Object => "object",
359            IntrinsicKind::Function => "Function",
360        }
361        .to_string()
362    }
363
364    fn format_literal(&mut self, lit: &LiteralValue) -> String {
365        match lit {
366            LiteralValue::String(s) => format!("\"{}\"", self.atom(*s)),
367            LiteralValue::Number(n) => format!("{}", n.0),
368            LiteralValue::BigInt(b) => format!("{}n", self.atom(*b)),
369            LiteralValue::Boolean(b) => if *b { "true" } else { "false" }.to_string(),
370        }
371    }
372
373    fn format_object(&mut self, props: &[PropertyInfo]) -> String {
374        if props.is_empty() {
375            return "{}".to_string();
376        }
377        if props.len() > 3 {
378            let first_three: Vec<String> = props
379                .iter()
380                .take(3)
381                .map(|p| self.format_property(p))
382                .collect();
383            return format!("{{ {}; ...; }}", first_three.join("; "));
384        }
385        let formatted: Vec<String> = props.iter().map(|p| self.format_property(p)).collect();
386        format!("{{ {}; }}", formatted.join("; "))
387    }
388
389    fn format_property(&mut self, prop: &PropertyInfo) -> String {
390        let optional = if prop.optional { "?" } else { "" };
391        let readonly = if prop.readonly { "readonly " } else { "" };
392        let type_str = self.format(prop.type_id);
393        let name = self.atom(prop.name);
394        format!("{readonly}{name}{optional}: {type_str}")
395    }
396
397    fn format_type_params(&mut self, type_params: &[TypeParamInfo]) -> String {
398        if type_params.is_empty() {
399            return String::new();
400        }
401
402        let mut parts = Vec::with_capacity(type_params.len());
403        for tp in type_params {
404            let mut part = String::new();
405            if tp.is_const {
406                part.push_str("const ");
407            }
408            part.push_str(self.atom(tp.name).as_ref());
409            if let Some(constraint) = tp.constraint {
410                part.push_str(" extends ");
411                part.push_str(&self.format(constraint));
412            }
413            if let Some(default) = tp.default {
414                part.push_str(" = ");
415                part.push_str(&self.format(default));
416            }
417            parts.push(part);
418        }
419
420        format!("<{}>", parts.join(", "))
421    }
422
423    fn format_params(&mut self, params: &[ParamInfo], this_type: Option<TypeId>) -> Vec<String> {
424        let mut rendered = Vec::with_capacity(params.len() + usize::from(this_type.is_some()));
425
426        if let Some(this_ty) = this_type {
427            rendered.push(format!("this: {}", self.format(this_ty)));
428        }
429
430        for p in params {
431            let name = p
432                .name
433                .map_or_else(|| "_".to_string(), |atom| self.atom(atom).to_string());
434            let optional = if p.optional { "?" } else { "" };
435            let rest = if p.rest { "..." } else { "" };
436            let type_str = self.format(p.type_id);
437            rendered.push(format!("{rest}{name}{optional}: {type_str}"));
438        }
439
440        rendered
441    }
442
443    /// Format a signature with the given separator between params and return type.
444    fn format_signature(
445        &mut self,
446        type_params: &[TypeParamInfo],
447        params: &[ParamInfo],
448        this_type: Option<TypeId>,
449        return_type: TypeId,
450        is_construct: bool,
451        separator: &str,
452    ) -> String {
453        let prefix = if is_construct { "new " } else { "" };
454        let type_params = self.format_type_params(type_params);
455        let params = self.format_params(params, this_type);
456        let return_str = if is_construct && return_type == TypeId::UNKNOWN {
457            "any".to_string()
458        } else {
459            self.format(return_type)
460        };
461        format!(
462            "{}{}({}) {} {}",
463            prefix,
464            type_params,
465            params.join(", "),
466            separator,
467            return_str
468        )
469    }
470
471    fn format_object_with_index(&mut self, shape: &ObjectShape) -> String {
472        let mut parts = Vec::new();
473
474        if let Some(ref idx) = shape.string_index {
475            parts.push(format!("[index: string]: {}", self.format(idx.value_type)));
476        }
477        if let Some(ref idx) = shape.number_index {
478            parts.push(format!("[index: number]: {}", self.format(idx.value_type)));
479        }
480        for prop in &shape.properties {
481            parts.push(self.format_property(prop));
482        }
483
484        format!("{{ {}; }}", parts.join("; "))
485    }
486
487    fn format_union(&mut self, members: &[TypeId]) -> String {
488        if members.len() > self.max_union_members {
489            let first: Vec<String> = members
490                .iter()
491                .take(self.max_union_members)
492                .map(|&m| self.format(m))
493                .collect();
494            return format!("{} | ...", first.join(" | "));
495        }
496        let formatted: Vec<String> = members.iter().map(|&m| self.format(m)).collect();
497        formatted.join(" | ")
498    }
499
500    fn format_intersection(&mut self, members: &[TypeId]) -> String {
501        let formatted: Vec<String> = members.iter().map(|&m| self.format(m)).collect();
502        formatted.join(" & ")
503    }
504
505    fn format_tuple(&mut self, elements: &[TupleElement]) -> String {
506        let formatted: Vec<String> = elements
507            .iter()
508            .map(|e| {
509                let rest = if e.rest { "..." } else { "" };
510                let optional = if e.optional { "?" } else { "" };
511                let type_str = self.format(e.type_id);
512                if let Some(name_atom) = e.name {
513                    let name = self.atom(name_atom);
514                    format!("{name}{optional}: {rest}{type_str}")
515                } else {
516                    format!("{rest}{type_str}{optional}")
517                }
518            })
519            .collect();
520        format!("[{}]", formatted.join(", "))
521    }
522
523    fn format_function(&mut self, shape: &FunctionShape) -> String {
524        self.format_signature(
525            &shape.type_params,
526            &shape.params,
527            shape.this_type,
528            shape.return_type,
529            shape.is_constructor,
530            "=>",
531        )
532    }
533
534    fn format_callable(&mut self, shape: &CallableShape) -> String {
535        if !shape.construct_signatures.is_empty()
536            && let Some(sym_id) = shape.symbol
537            && let Some(name) = self.format_symbol_name(sym_id)
538        {
539            return format!("typeof {name}");
540        }
541
542        let has_index = shape.string_index.is_some() || shape.number_index.is_some();
543        if !has_index && shape.properties.is_empty() {
544            if shape.call_signatures.len() == 1 && shape.construct_signatures.is_empty() {
545                let sig = &shape.call_signatures[0];
546                return self.format_signature(
547                    &sig.type_params,
548                    &sig.params,
549                    sig.this_type,
550                    sig.return_type,
551                    false,
552                    "=>",
553                );
554            }
555            if shape.construct_signatures.len() == 1 && shape.call_signatures.is_empty() {
556                let sig = &shape.construct_signatures[0];
557                return self.format_signature(
558                    &sig.type_params,
559                    &sig.params,
560                    sig.this_type,
561                    sig.return_type,
562                    true,
563                    "=>",
564                );
565            }
566        }
567
568        let mut parts = Vec::new();
569        for sig in &shape.call_signatures {
570            parts.push(self.format_call_signature(sig, false));
571        }
572        for sig in &shape.construct_signatures {
573            parts.push(self.format_call_signature(sig, true));
574        }
575        if let Some(ref idx) = shape.string_index {
576            parts.push(format!("[index: string]: {}", self.format(idx.value_type)));
577        }
578        if let Some(ref idx) = shape.number_index {
579            parts.push(format!("[index: number]: {}", self.format(idx.value_type)));
580        }
581        let mut sorted_props: Vec<&PropertyInfo> = shape.properties.iter().collect();
582        sorted_props.sort_by(|a, b| {
583            self.interner
584                .resolve_atom_ref(a.name)
585                .cmp(&self.interner.resolve_atom_ref(b.name))
586        });
587        for prop in sorted_props {
588            parts.push(self.format_property(prop));
589        }
590
591        if parts.is_empty() {
592            return "{}".to_string();
593        }
594
595        format!("{{ {}; }}", parts.join("; "))
596    }
597
598    fn format_call_signature(&mut self, sig: &CallSignature, is_construct: bool) -> String {
599        self.format_signature(
600            &sig.type_params,
601            &sig.params,
602            sig.this_type,
603            sig.return_type,
604            is_construct,
605            ":",
606        )
607    }
608
609    fn format_conditional(&mut self, cond: &ConditionalType) -> String {
610        format!(
611            "{} extends {} ? {} : {}",
612            self.format(cond.check_type),
613            self.format(cond.extends_type),
614            self.format(cond.true_type),
615            self.format(cond.false_type)
616        )
617    }
618
619    fn format_mapped(&mut self, mapped: &MappedType) -> String {
620        format!(
621            "{{ [K in {}]: {} }}",
622            self.format(mapped.constraint),
623            self.format(mapped.template)
624        )
625    }
626
627    fn format_template_literal(&mut self, spans: &[TemplateSpan]) -> String {
628        let mut result = String::from("`");
629        for span in spans {
630            match span {
631                TemplateSpan::Text(text) => {
632                    let text = self.atom(*text);
633                    result.push_str(text.as_ref());
634                }
635                TemplateSpan::Type(type_id) => {
636                    result.push_str("${");
637                    result.push_str(&self.format(*type_id));
638                    result.push('}');
639                }
640            }
641        }
642        result.push('`');
643        result
644    }
645
646    /// Resolve a `DefId` to a human-readable name via the definition store,
647    /// falling back to `"<prefix>(<raw_id>)"` if unavailable.
648    fn format_def_id(&mut self, def_id: crate::def::DefId, fallback_prefix: &str) -> String {
649        if let Some(def_store) = self.def_store
650            && let Some(def) = def_store.get(def_id)
651        {
652            return self.format_def_name(&def);
653        }
654        format!("{}({})", fallback_prefix, def_id.0)
655    }
656
657    /// Try to resolve a human-readable name for an object shape via symbol or def store lookup.
658    fn resolve_object_shape_name(&mut self, shape: &ObjectShape) -> Option<String> {
659        if let Some(sym_id) = shape.symbol
660            && let Some(name) = self.format_symbol_name(sym_id)
661        {
662            return Some(name);
663        }
664        if let Some(def_store) = self.def_store
665            && let Some(def_id) = def_store.find_def_by_shape(shape)
666            && let Some(def) = def_store.get(def_id)
667        {
668            return Some(self.format_def_name(&def));
669        }
670        None
671    }
672
673    fn format_symbol_name(&mut self, sym_id: SymbolId) -> Option<String> {
674        let arena = self.symbol_arena?;
675        let sym = arena.get(sym_id)?;
676        let mut qualified_name = sym.escaped_name.to_string();
677        let mut current_parent = sym.parent;
678
679        while current_parent != SymbolId::NONE {
680            if let Some(parent_sym) = arena.get(current_parent) {
681                // Don't prepend for source files and blocks
682                if !parent_sym.escaped_name.starts_with('"')
683                    && !parent_sym.escaped_name.starts_with("__")
684                {
685                    qualified_name = format!("{}.{}", parent_sym.escaped_name, qualified_name);
686                }
687                current_parent = parent_sym.parent;
688            } else {
689                break;
690            }
691        }
692
693        Some(qualified_name)
694    }
695
696    fn format_def_name(&mut self, def: &crate::def::DefinitionInfo) -> String {
697        if let Some(sym_id) = def.symbol_id
698            && let Some(qualified_name) = self.format_symbol_name(SymbolId(sym_id))
699        {
700            return qualified_name;
701        }
702
703        self.atom(def.name).to_string()
704    }
705}