1use std::{
7 borrow::Cow,
8 collections::{BTreeSet, HashMap, HashSet, hash_map::Entry},
9};
10
11use specta::{
12 Format, Types,
13 datatype::{
14 DataType, Deprecated, Enum, Field, Fields, GenericDefinition, GenericReference, List, Map,
15 NamedDataType, NamedReference, NamedReferenceType, OpaqueReference, Primitive, Reference,
16 Struct, Tuple, Variant,
17 },
18};
19
20use crate::{
21 Branded, BrandedTypeExporter, Error, Exporter, Layout, map_keys, opaque,
22 reserved_names::RESERVED_TYPE_NAMES,
23};
24
25const STRING: &str = "string";
26const NULL: &str = "null";
27const NEVER: &str = "never";
28const FIELD_ALIAS_UNION_MARKER: &str = "specta_serde:deferred_alias_union";
29const FIELD_ALIAS_EXCLUSION_MARKER: &str = "specta_serde:alias_exclusion";
30
31fn path_string(location: &[Cow<'static, str>]) -> String {
32 location.join(".")
33}
34
35fn rust_type_path(ndt: &NamedDataType) -> Cow<'static, str> {
36 if ndt.module_path.is_empty() {
37 ndt.name.clone()
38 } else {
39 Cow::Owned(format!("{}::{}", ndt.module_path, ndt.name))
40 }
41}
42
43fn module_prefixed_type_name(ndt: &NamedDataType) -> String {
44 let mut name = ndt.module_path.split("::").collect::<Vec<_>>().join("_");
45 if !name.is_empty() {
46 name.push('_');
47 }
48 name.push_str(&ndt.name);
49 name
50}
51
52fn exported_type_name<'a>(exporter: &Exporter, ndt: &'a NamedDataType) -> Cow<'a, str> {
53 match exporter.layout {
54 Layout::ModulePrefixedName => Cow::Owned(module_prefixed_type_name(ndt)),
55 _ => ndt.name.clone(),
56 }
57}
58
59fn referenced_type_name<'a>(exporter: &Exporter, ndt: &'a NamedDataType) -> Cow<'a, str> {
60 match exporter.layout {
61 Layout::ModulePrefixedName => Cow::Owned(module_prefixed_type_name(ndt)),
62 Layout::Namespaces => {
63 if ndt.module_path.is_empty() {
64 ndt.name.clone()
65 } else {
66 let mut path =
67 ndt.module_path
68 .split("::")
69 .fold("$s$.".to_string(), |mut s, segment| {
70 s.push_str(segment);
71 s.push('.');
72 s
73 });
74 path.push_str(&ndt.name);
75 Cow::Owned(path)
76 }
77 }
78 Layout::Files => {
79 let current_module_path = crate::references::current_module_path().unwrap_or_default();
80
81 if ndt.module_path == current_module_path {
82 ndt.name.clone()
83 } else {
84 let mut path = crate::exporter::module_alias(&ndt.module_path);
85 path.push('.');
86 path.push_str(&ndt.name);
87 Cow::Owned(path)
88 }
89 }
90 _ => ndt.name.clone(),
91 }
92}
93
94fn inner_comments(
95 deprecated: Option<&Deprecated>,
96 docs: &str,
97 other: String,
98 start_with_newline: bool,
99 prefix: &str,
100) -> String {
101 let mut comments = String::new();
102 js_doc(&mut comments, docs, deprecated);
103 if comments.is_empty() {
104 return other;
105 }
106
107 let mut out = String::new();
108 if start_with_newline {
109 out.push('\n');
110 }
111
112 for line in comments.lines() {
113 out.push_str(prefix);
114 out.push_str(line);
115 out.push('\n');
116 }
117
118 out.push_str(&other);
119 out
120}
121
122pub(crate) fn is_identifier(name: &str) -> bool {
123 let mut chars = name.chars();
124 let Some(first) = chars.next() else {
125 return false;
126 };
127
128 (first.is_ascii_alphabetic() || first == '_' || first == '$')
129 && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$')
130}
131
132pub(crate) fn escape_typescript_string_literal(value: &str) -> Cow<'_, str> {
133 if !value.chars().any(|ch| {
134 ch == '"' || ch == '\\' || ch == '\u{2028}' || ch == '\u{2029}' || ch.is_control()
135 }) {
136 return Cow::Borrowed(value);
137 }
138
139 let mut escaped = String::with_capacity(value.len());
140 for ch in value.chars() {
141 match ch {
142 '"' => escaped.push_str(r#"\""#),
143 '\\' => escaped.push_str(r#"\\"#),
144 '\n' => escaped.push_str(r#"\n"#),
145 '\r' => escaped.push_str(r#"\r"#),
146 '\t' => escaped.push_str(r#"\t"#),
147 '\u{2028}' => escaped.push_str(r#"\u2028"#),
148 '\u{2029}' => escaped.push_str(r#"\u2029"#),
149 ch if ch.is_control() => push_unicode_escape(&mut escaped, ch),
150 _ => escaped.push(ch),
151 }
152 }
153
154 Cow::Owned(escaped)
155}
156
157fn push_unicode_escape(s: &mut String, ch: char) {
158 const HEX: &[u8; 16] = b"0123456789ABCDEF";
159 let value = ch as u32;
160
161 s.push_str(r#"\u"#);
162 s.push(HEX[((value >> 12) & 0xF) as usize] as char);
163 s.push(HEX[((value >> 8) & 0xF) as usize] as char);
164 s.push(HEX[((value >> 4) & 0xF) as usize] as char);
165 s.push(HEX[(value & 0xF) as usize] as char);
166}
167
168fn sanitise_key<'a>(field_name: Cow<'static, str>, force_string: bool) -> Cow<'a, str> {
169 if force_string || !is_identifier(&field_name) {
170 format!(r#""{}""#, escape_typescript_string_literal(&field_name)).into()
171 } else {
172 field_name
173 }
174}
175
176fn sanitise_type_name(location: &[Cow<'static, str>], ident: &str) -> Result<String, Error> {
177 let path = path_string(location);
178
179 if ident.is_empty() {
180 return Err(Error::empty_name(path));
181 }
182
183 if let Some(name) = RESERVED_TYPE_NAMES.iter().find(|v| **v == ident) {
184 return Err(Error::forbidden_name(path, name));
185 }
186
187 if let Some(first_char) = ident.chars().next()
188 && !first_char.is_alphabetic()
189 && first_char != '_'
190 {
191 return Err(Error::invalid_name(path, ident.to_string()));
192 }
193
194 if ident
195 .find(|c: char| !c.is_alphanumeric() && c != '_')
196 .is_some()
197 {
198 return Err(Error::invalid_name(path, ident.to_string()));
199 }
200
201 Ok(ident.to_string())
202}
203
204pub(crate) fn js_doc(s: &mut String, docs: &str, deprecated: Option<&Deprecated>) {
205 if docs.is_empty() && deprecated.is_none() {
206 return;
207 }
208
209 if deprecated.is_none() {
210 let mut lines = docs.lines();
211 if let (Some(line), None) = (lines.next(), lines.next()) {
212 s.push_str("/** ");
213 s.push_str(&escape_jsdoc_text(line));
214 s.push_str(" */\n");
215 return;
216 }
217 }
218
219 s.push_str("/**\n");
220 if !docs.is_empty() {
221 for line in docs.lines() {
222 s.push_str(" * ");
223 s.push_str(&escape_jsdoc_text(line));
224 s.push('\n');
225 }
226 }
227
228 if let Some(typ) = deprecated {
229 s.push_str(" * @deprecated");
230 if let Some(details) = deprecated_details(typ) {
231 s.push(' ');
232 s.push_str(&details);
233 }
234 s.push('\n');
235 }
236
237 s.push_str(" */\n");
238}
239
240pub(crate) fn escape_jsdoc_text(text: &str) -> Cow<'_, str> {
241 if text.contains("*/") {
242 Cow::Owned(text.replace("*/", "*\\/"))
243 } else {
244 Cow::Borrowed(text)
245 }
246}
247
248pub(crate) fn deprecated_details(typ: &Deprecated) -> Option<String> {
249 typ.note
250 .as_deref()
251 .map(str::trim)
252 .filter(|note| !note.is_empty())
253 .map(str::to_string)
254}
255
256pub fn export<'a>(
271 exporter: &dyn AsRef<Exporter>,
272 types: &Types,
273 ndts: impl Iterator<Item = &'a NamedDataType>,
274 indent: &str,
275) -> Result<String, Error> {
276 let mut s = String::new();
277 export_internal(&mut s, exporter.as_ref(), None, types, ndts, indent)?;
278 Ok(s)
279}
280
281pub(crate) fn export_internal<'a>(
282 s: &mut String,
283 exporter: &Exporter,
284 format: Option<&dyn Format>,
285 types: &Types,
286 ndts: impl Iterator<Item = &'a NamedDataType>,
287 indent: &str,
288) -> Result<(), Error> {
289 let ndts = ndts.filter(|ndt| ndt.ty.is_some());
290
291 if exporter.jsdoc {
292 let mut ndts = ndts.peekable();
293 if ndts.peek().is_none() {
294 return Ok(());
295 }
296
297 s.push_str(indent);
298 s.push_str("/**\n");
299
300 for (index, ndt) in ndts.enumerate() {
301 if index != 0 {
302 s.push_str(indent);
303 s.push_str("\t*\n");
304 }
305
306 append_typedef_body(s, exporter, format, types, ndt, indent)?;
307 }
308
309 s.push_str(indent);
310 s.push_str("\t*/\n");
311 return Ok(());
312 }
313
314 for (index, ndt) in ndts.enumerate() {
315 if index != 0 {
316 s.push('\n');
317 }
318
319 export_single_internal(s, exporter, format, types, ndt, indent)?;
320 }
321
322 Ok(())
323}
324
325fn export_single_internal(
326 s: &mut String,
327 exporter: &Exporter,
328 format: Option<&dyn Format>,
329 types: &Types,
330 ndt: &NamedDataType,
331 indent: &str,
332) -> Result<(), Error> {
333 if exporter.jsdoc {
334 let mut typedef = String::new();
335 typedef_internal(&mut typedef, exporter, format, types, ndt)?;
336 for line in typedef.lines() {
337 s.push_str(indent);
338 s.push_str(line);
339 s.push('\n');
340 }
341 return Ok(());
342 }
343
344 let raw_name = exported_type_name(exporter, ndt);
345 let name = sanitise_type_name(&[rust_type_path(ndt)], &raw_name)
346 .map_err(|err| err.with_named_datatype(ndt))?;
347
348 let mut comments = String::new();
349 js_doc(&mut comments, &ndt.docs, ndt.deprecated.as_ref());
350 if !comments.is_empty() {
351 for line in comments.lines() {
352 s.push_str(indent);
353 s.push_str(line);
354 s.push('\n');
355 }
356 }
357
358 s.push_str(indent);
359 s.push_str("export type ");
360 s.push_str(&name);
361 write_generic_parameters(s, exporter, types, &[rust_type_path(ndt)], &ndt.generics)?;
362 s.push_str(" = ");
363
364 let body = resolve_named_export_body(types, ndt)?;
365 datatype(
366 s,
367 exporter,
368 format,
369 types,
370 &body,
371 vec![rust_type_path(ndt)],
372 Some(ndt.name.as_ref()),
373 indent,
374 Default::default(),
375 )
376 .map_err(|err| err.with_named_datatype(ndt))?;
377 s.push_str(";\n");
378
379 Ok(())
380}
381
382pub fn inline(
393 exporter: &dyn AsRef<Exporter>,
394 types: &Types,
395 dt: &DataType,
396) -> Result<String, Error> {
397 let mut s = String::new();
398 inline_datatype(
399 &mut s,
400 exporter.as_ref(),
401 None,
402 types,
403 dt,
404 vec![],
405 None,
406 "",
407 0,
408 &[],
409 )?;
410 Ok(s)
411}
412
413pub(crate) fn typedef_internal(
416 s: &mut String,
417 exporter: &Exporter,
418 format: Option<&dyn Format>,
419 types: &Types,
420 dt: &NamedDataType,
421) -> Result<(), Error> {
422 s.push_str("/**\n");
423 append_typedef_body(s, exporter, format, types, dt, "")?;
424
425 s.push_str("\t*/");
426
427 Ok(())
428}
429
430fn append_jsdoc_properties(
431 s: &mut String,
432 exporter: &Exporter,
433 format: Option<&dyn Format>,
434 types: &Types,
435 dt_name: &str,
436 dt: &DataType,
437 indent: &str,
438) -> Result<(), Error> {
439 match dt {
440 DataType::Struct(strct) => match &strct.fields {
441 Fields::Unit => {}
442 Fields::Unnamed(unnamed) => {
443 for (idx, field) in unnamed.fields.iter().enumerate() {
444 let Some(ty) = field.ty.as_ref() else {
445 continue;
446 };
447
448 let mut ty_str = String::new();
449 let datatype_prefix = format!("{indent}\t*\t");
450 datatype(
451 &mut ty_str,
452 exporter,
453 format,
454 types,
455 ty,
456 vec![Cow::Owned(dt_name.to_owned()), idx.to_string().into()],
457 Some(dt_name),
458 &datatype_prefix,
459 Default::default(),
460 )?;
461
462 push_jsdoc_property(
463 s,
464 &ty_str,
465 &idx.to_string(),
466 field.optional,
467 &field.docs,
468 field.deprecated.as_ref(),
469 indent,
470 );
471 }
472 }
473 Fields::Named(named) => {
474 for (name, field) in &named.fields {
475 let Some(ty) = field.ty.as_ref() else {
476 continue;
477 };
478
479 let mut ty_str = String::new();
480 let datatype_prefix = format!("{indent}\t*\t");
481 datatype(
482 &mut ty_str,
483 exporter,
484 format,
485 types,
486 ty,
487 vec![Cow::Owned(dt_name.to_owned()), name.clone()],
488 Some(dt_name),
489 &datatype_prefix,
490 Default::default(),
491 )?;
492
493 push_jsdoc_property(
494 s,
495 &ty_str,
496 name,
497 field.optional,
498 &field.docs,
499 field.deprecated.as_ref(),
500 indent,
501 );
502 }
503 }
504 },
505 DataType::Enum(enm) => {
506 for (variant_name, variant) in enm.variants.iter().filter(|(_, v)| !v.skip) {
507 let mut one_variant_enum = enm.clone();
508 one_variant_enum
509 .variants
510 .retain(|(name, _)| name == variant_name);
511
512 let mut variant_ty = String::new();
513 enum_dt(
514 &mut variant_ty,
515 exporter,
516 types,
517 &one_variant_enum,
518 vec![Cow::Owned(dt_name.to_owned())],
519 "",
520 &[],
521 )?;
522
523 push_jsdoc_property(
524 s,
525 &variant_ty,
526 variant_name,
527 false,
528 &variant.docs,
529 variant.deprecated.as_ref(),
530 indent,
531 );
532 }
533 }
534 DataType::Intersection(types_) => {
535 for ty in types_ {
536 append_jsdoc_properties(s, exporter, format, types, dt_name, ty, indent)?;
537 }
538 }
539 _ => {}
540 }
541
542 Ok(())
543}
544
545fn push_jsdoc_property(
546 s: &mut String,
547 ty: &str,
548 name: &str,
549 optional: bool,
550 docs: &str,
551 deprecated: Option<&Deprecated>,
552 indent: &str,
553) {
554 s.push_str(indent);
555 s.push_str("\t* @property {");
556 push_jsdoc_type(s, ty, indent);
557 s.push_str("} ");
558 s.push_str(&jsdoc_property_name(name, optional));
559
560 if let Some(description) = jsdoc_description(docs, deprecated) {
561 s.push_str(" - ");
562 s.push_str(&description);
563 }
564
565 s.push('\n');
566}
567
568fn push_jsdoc_type(s: &mut String, ty: &str, indent: &str) {
569 let mut lines = ty.lines();
570 if let Some(first_line) = lines.next() {
571 s.push_str(first_line);
572 }
573
574 for line in lines {
575 s.push('\n');
576
577 if line
578 .strip_prefix(indent)
579 .is_some_and(|rest| rest.starts_with("\t*"))
580 {
581 s.push_str(line);
582 } else {
583 s.push_str(indent);
584 s.push_str("\t* ");
585 s.push_str(line);
586 }
587 }
588}
589
590fn jsdoc_property_name(name: &str, optional: bool) -> String {
591 let name = if is_identifier(name) {
592 name.to_string()
593 } else {
594 format!("\"{}\"", escape_typescript_string_literal(name))
595 };
596
597 if optional { format!("[{name}]") } else { name }
598}
599
600fn append_typedef_body(
601 s: &mut String,
602 exporter: &Exporter,
603 format: Option<&dyn Format>,
604 types: &Types,
605 dt: &NamedDataType,
606 indent: &str,
607) -> Result<(), Error> {
608 let name = &dt.name;
609 let mut type_name = String::from(name.as_ref());
610 write_generic_parameters(
611 &mut type_name,
612 exporter,
613 types,
614 &[rust_type_path(dt)],
615 &dt.generics,
616 )?;
617
618 let mut typedef_ty = String::new();
619 let datatype_prefix = format!("{indent}\t*\t");
620 let body = resolve_named_export_body(types, dt)?;
621 datatype(
622 &mut typedef_ty,
623 exporter,
624 format,
625 types,
626 &body,
627 vec![rust_type_path(dt)],
628 Some(dt.name.as_ref()),
629 &datatype_prefix,
630 Default::default(),
631 )
632 .map_err(|err| err.with_named_datatype(dt))?;
633
634 if !dt.docs.is_empty() {
635 for line in dt.docs.lines() {
636 s.push_str(indent);
637 s.push_str("\t* ");
638 s.push_str(&escape_jsdoc_text(line));
639 s.push('\n');
640 }
641 s.push_str(indent);
642 s.push_str("\t*\n");
643 }
644
645 if let Some(deprecated) = dt.deprecated.as_ref() {
646 s.push_str(indent);
647 s.push_str("\t* @deprecated");
648 if let Some(details) = deprecated_details(deprecated) {
649 s.push(' ');
650 s.push_str(&details);
651 }
652 s.push('\n');
653 }
654
655 s.push_str(indent);
656 s.push_str("\t* @typedef {");
657 push_jsdoc_type(s, &typedef_ty, indent);
658 s.push_str("} ");
659 s.push_str(&type_name);
660 s.push('\n');
661
662 if let Some(ty) = &dt.ty {
663 let dt_path = rust_type_path(dt);
664 append_jsdoc_properties(s, exporter, format, types, dt_path.as_ref(), ty, indent)?;
665 }
666
667 Ok(())
668}
669
670fn write_generic_parameters(
671 s: &mut String,
672 exporter: &Exporter,
673 types: &Types,
674 parent_location: &[Cow<'static, str>],
675 generics: &[GenericDefinition],
676) -> Result<(), Error> {
677 if generics.is_empty() {
678 return Ok(());
679 }
680
681 s.push('<');
682 for (index, generic) in generics.iter().enumerate() {
683 if index != 0 {
684 s.push_str(", ");
685 }
686
687 s.push_str(generic.name.as_ref());
688
689 if let Some(default) = &generic.default {
690 let mut rendered_default = String::new();
691 let mut default_location = parent_location.to_vec();
692 default_location.push(format!("<generic {} default>", generic.name).into());
693 shallow_inline_datatype(
694 &mut rendered_default,
695 exporter,
696 None,
697 types,
698 default,
699 default_location,
700 None,
701 "",
702 Default::default(),
703 )?;
704 s.push_str(" = ");
705 s.push_str(&rendered_default);
706 }
707 }
708 s.push('>');
709
710 Ok(())
711}
712
713fn jsdoc_description(docs: &str, deprecated: Option<&Deprecated>) -> Option<String> {
714 let docs = docs
715 .lines()
716 .map(str::trim)
717 .filter(|line| !line.is_empty())
718 .map(|line| escape_jsdoc_text(line).into_owned())
719 .collect::<Vec<_>>()
720 .join(" ");
721
722 let deprecated = deprecated.map(|deprecated| {
723 let mut value = String::from("@deprecated");
724 if let Some(details) = deprecated_details(deprecated) {
725 value.push(' ');
726 value.push_str(&escape_jsdoc_text(&details));
727 }
728 value
729 });
730
731 match (docs.is_empty(), deprecated) {
732 (true, None) => None,
733 (true, Some(deprecated)) => Some(deprecated),
734 (false, None) => Some(docs),
735 (false, Some(deprecated)) => Some(format!("{docs} {deprecated}")),
736 }
737}
738
739pub fn reference(
747 exporter: &dyn AsRef<Exporter>,
748 types: &Types,
749 dt: &DataType,
750) -> Result<String, Error> {
751 let mut s = String::new();
752 datatype(
753 &mut s,
754 exporter.as_ref(),
755 None,
756 types,
757 dt,
758 vec![],
759 None,
760 "",
761 &[],
762 )?;
763 Ok(s)
764}
765
766pub(crate) fn datatype_with_inline_attr(
767 s: &mut String,
768 exporter: &Exporter,
769 format: Option<&dyn Format>,
770 types: &Types,
771 dt: &DataType,
772 location: Vec<Cow<'static, str>>,
773 parent_name: Option<&str>,
774 prefix: &str,
775 generics: &[(GenericReference, DataType)],
776 shallow_inline: bool,
777) -> Result<(), Error> {
778 if shallow_inline {
779 let inline_path = path_string(&location);
780 return shallow_inline_datatype(
781 s,
782 exporter,
783 format,
784 types,
785 dt,
786 location,
787 parent_name,
788 prefix,
789 generics,
790 )
791 .map_err(|err| err.with_inline_trace(inline_named_datatype(types, dt), inline_path));
792 }
793
794 datatype(
795 s,
796 exporter,
797 format,
798 types,
799 dt,
800 location,
801 parent_name,
802 prefix,
803 generics,
804 )
805}
806
807fn write_generic_reference(s: &mut String, generic: &GenericReference) {
808 s.push_str(generic.name());
809}
810
811fn scoped_reference_generics(
812 parent_generics: &[(GenericReference, DataType)],
813 reference_generics: &[(GenericReference, DataType)],
814) -> Vec<(GenericReference, DataType)> {
815 parent_generics
816 .iter()
817 .filter(|(parent_generic, _)| {
818 !reference_generics
819 .iter()
820 .any(|(child_generic, _)| child_generic == parent_generic)
821 })
822 .cloned()
823 .collect()
824}
825
826fn named_reference_generics(r: &NamedReference) -> Result<&[(GenericReference, DataType)], Error> {
827 match &r.inner {
828 NamedReferenceType::Reference { generics, .. } => Ok(generics),
829 NamedReferenceType::Inline { .. } => Ok(&[]),
830 NamedReferenceType::Recursive(_) => Ok(&[]),
831 }
832}
833
834fn named_reference_ty<'a>(
835 types: &'a Types,
836 r: &'a NamedReference,
837 location: &[Cow<'static, str>],
838) -> Result<&'a DataType, Error> {
839 let path = path_string(location);
840 match &r.inner {
841 NamedReferenceType::Reference { .. } => types
842 .get(r)
843 .and_then(|ndt| ndt.ty.as_ref())
844 .ok_or_else(|| Error::dangling_named_reference(path, format!("{r:?}"))),
845 NamedReferenceType::Inline { dt, .. } => Ok(dt),
846 NamedReferenceType::Recursive(cycle) => Err(Error::infinite_recursive_inline_type(
847 path,
848 format!("{r:?}"),
849 cycle.clone(),
850 )),
851 }
852}
853
854fn inline_named_datatype<'a>(types: &'a Types, dt: &DataType) -> Option<&'a NamedDataType> {
855 match dt {
856 DataType::Reference(Reference::Named(r)) => types.get(r),
857 _ => None,
858 }
859}
860
861fn resolve_scoped_generic_default(
862 default: &DataType,
863 scoped_generics: &[(GenericReference, DataType)],
864) -> DataType {
865 match default {
866 DataType::Generic(default) => scoped_generics
867 .iter()
868 .find_map(|(reference, dt)| (reference == default).then_some(dt.clone()))
869 .unwrap_or_else(|| DataType::Generic(default.clone())),
870 default => default.clone(),
871 }
872}
873
874fn resolved_reference_generics(
875 ndt: &specta::datatype::NamedDataType,
876 r: &NamedReference,
877 parent_generics: &[(GenericReference, DataType)],
878) -> Option<(Vec<DataType>, bool, Vec<(GenericReference, DataType)>)> {
879 let reference_generics = named_reference_generics(r).ok()?;
880 let mut scoped_generics = scoped_reference_generics(parent_generics, reference_generics);
881 let mut all_default = true;
882 let mut rendered_generics = Vec::with_capacity(ndt.generics.len());
883
884 for generic in ndt.generics.iter() {
885 let explicit = reference_generics
886 .iter()
887 .find(|(reference, _)| *reference == generic.reference())
888 .map(|(_, dt)| dt.clone());
889
890 let resolved_default = generic
891 .default
892 .as_ref()
893 .map(|default| resolve_scoped_generic_default(default, &scoped_generics));
894
895 let resolved = explicit.or_else(|| resolved_default.clone()).or_else(|| {
896 Some(DataType::Reference(Reference::opaque(
897 crate::opaque::Unknown,
898 )))
899 });
900
901 let resolved = resolved?;
902 all_default &= resolved_default
903 .as_ref()
904 .is_some_and(|default| default == &resolved);
905 scoped_generics.push((generic.reference(), resolved.clone()));
906 rendered_generics.push(resolved);
907 }
908
909 Some((rendered_generics, all_default, scoped_generics))
910}
911
912#[derive(Clone, Copy)]
913struct RenderCtx<'a> {
914 exporter: &'a Exporter,
915 format: Option<&'a dyn Format>,
916 types: &'a Types,
917 parent_name: Option<&'a str>,
918 prefix: &'a str,
919 generics: &'a [(GenericReference, DataType)],
920}
921
922#[derive(Clone, Copy)]
923enum RenderMode {
924 Normal,
925 ShallowInline,
926}
927
928impl RenderMode {
929 fn render(
930 self,
931 s: &mut String,
932 ctx: RenderCtx<'_>,
933 dt: &DataType,
934 location: Vec<Cow<'static, str>>,
935 ) -> Result<(), Error> {
936 match self {
937 Self::Normal => datatype(
938 s,
939 ctx.exporter,
940 ctx.format,
941 ctx.types,
942 dt,
943 location,
944 ctx.parent_name,
945 ctx.prefix,
946 ctx.generics,
947 ),
948 Self::ShallowInline => shallow_inline_datatype(
949 s,
950 ctx.exporter,
951 ctx.format,
952 ctx.types,
953 dt,
954 location,
955 ctx.parent_name,
956 ctx.prefix,
957 ctx.generics,
958 ),
959 }
960 }
961
962 fn render_intersection_part(
963 self,
964 s: &mut String,
965 ctx: RenderCtx<'_>,
966 dt: &DataType,
967 location: Vec<Cow<'static, str>>,
968 ) -> Result<(), Error> {
969 match (self, dt) {
970 (Self::ShallowInline, DataType::Reference(r)) => reference_dt(
971 s,
972 ctx.exporter,
973 ctx.format,
974 ctx.types,
975 r,
976 location,
977 ctx.prefix,
978 ctx.generics,
979 ),
980 _ => self.render(s, ctx, dt, location),
981 }
982 }
983}
984
985fn render_datatype(
986 s: &mut String,
987 ctx: RenderCtx<'_>,
988 dt: &DataType,
989 location: Vec<Cow<'static, str>>,
990 mode: RenderMode,
991) -> Result<(), Error> {
992 match (mode, dt) {
993 (_, DataType::Primitive(p)) => s.push_str(primitive_dt(p, location)?),
994 (_, DataType::Generic(g)) => write_generic_reference(s, g),
995 (RenderMode::Normal, DataType::List(list)) => {
996 list_dt(s, ctx.exporter, ctx.types, list, location, ctx.generics)?;
997 }
998 (RenderMode::ShallowInline, DataType::List(list)) => {
999 let mut inner = String::new();
1000 render_datatype(&mut inner, ctx, &list.ty, location, mode)?;
1001 push_list(s, &inner, list.length);
1002 }
1003 (RenderMode::Normal, DataType::Map(map)) => {
1004 map_dt(
1005 s,
1006 ctx.exporter,
1007 ctx.format,
1008 ctx.types,
1009 map,
1010 location,
1011 ctx.generics,
1012 )?;
1013 }
1014 (RenderMode::ShallowInline, DataType::Map(map)) => render_map(
1015 s,
1016 ctx.exporter,
1017 ctx.format,
1018 ctx.types,
1019 map,
1020 location,
1021 ctx.parent_name,
1022 ctx.prefix,
1023 ctx.generics,
1024 mode,
1025 )?,
1026 (_, DataType::Nullable(inner)) => {
1027 let mut rendered = String::new();
1028 let child_ctx = RenderCtx { prefix: "", ..ctx };
1029 render_datatype(&mut rendered, child_ctx, inner, location, mode)?;
1030 push_nullable(s, &rendered);
1031 }
1032 (_, DataType::Struct(st)) => struct_dt(
1033 s,
1034 ctx.exporter,
1035 ctx.format,
1036 ctx.types,
1037 st,
1038 location,
1039 ctx.parent_name,
1040 ctx.prefix,
1041 ctx.generics,
1042 )?,
1043 (_, DataType::Enum(enm)) => enum_dt(
1044 s,
1045 ctx.exporter,
1046 ctx.types,
1047 enm,
1048 location,
1049 ctx.prefix,
1050 ctx.generics,
1051 )?,
1052 (RenderMode::Normal, DataType::Tuple(tuple)) => {
1053 tuple_dt(s, ctx.exporter, ctx.types, tuple, location, ctx.generics)?;
1054 }
1055 (RenderMode::ShallowInline, DataType::Tuple(tuple)) => match tuple.elements.as_slice() {
1056 [] => s.push_str(NULL),
1057 elements => {
1058 s.push('[');
1059 for (idx, dt) in elements.iter().enumerate() {
1060 if idx != 0 {
1061 s.push_str(", ");
1062 }
1063 render_datatype(s, ctx, dt, location.clone(), mode)?;
1064 }
1065 s.push(']');
1066 }
1067 },
1068 (RenderMode::Normal, DataType::Intersection(parts)) => {
1069 for (idx, ty) in parts.iter().enumerate() {
1070 if idx != 0 {
1071 s.push_str(" & ");
1072 }
1073
1074 let needs_parentheses = parts.len() > 1 && intersection_part_is_union(ty);
1075 if needs_parentheses {
1076 s.push('(');
1077 }
1078 render_datatype(s, ctx, ty, location.clone(), mode)?;
1079 if needs_parentheses {
1080 s.push(')');
1081 }
1082 }
1083 }
1084 (RenderMode::ShallowInline, DataType::Intersection(parts)) => intersection_dt(
1085 s,
1086 ctx.exporter,
1087 ctx.format,
1088 ctx.types,
1089 parts,
1090 location,
1091 ctx.parent_name,
1092 ctx.prefix,
1093 ctx.generics,
1094 mode,
1095 )?,
1096 (RenderMode::Normal, DataType::Reference(r)) => reference_dt(
1097 s,
1098 ctx.exporter,
1099 ctx.format,
1100 ctx.types,
1101 r,
1102 location,
1103 ctx.prefix,
1104 ctx.generics,
1105 )?,
1106 (RenderMode::ShallowInline, DataType::Reference(r)) => match r {
1107 Reference::Named(r) => {
1108 let ty = named_reference_ty(ctx.types, r, &location)?;
1109 let reference_generics = named_reference_generics(r)?;
1110 let child_ctx = RenderCtx {
1111 generics: reference_generics,
1112 ..ctx
1113 };
1114 let inline_path = path_string(&location);
1115 render_datatype(s, child_ctx, ty, location, mode)
1116 .map_err(|err| err.with_inline_trace(ctx.types.get(r), inline_path))?;
1117 }
1118 Reference::Opaque(_) => reference_dt(
1119 s,
1120 ctx.exporter,
1121 ctx.format,
1122 ctx.types,
1123 r,
1124 location,
1125 ctx.prefix,
1126 ctx.generics,
1127 )?,
1128 },
1129 }
1130
1131 Ok(())
1132}
1133
1134fn needs_array_parens(ty: &str) -> bool {
1135 ty.contains(' ') && (!ty.ends_with('}') || ty.contains('&') || ty.contains('|'))
1136}
1137
1138fn push_list(s: &mut String, ty: &str, length: Option<usize>) {
1139 let ty = if needs_array_parens(ty) {
1140 Cow::Owned(format!("({ty})"))
1141 } else {
1142 Cow::Borrowed(ty)
1143 };
1144
1145 if let Some(length) = length {
1146 s.push('[');
1147 for i in 0..length {
1148 if i != 0 {
1149 s.push_str(", ");
1150 }
1151 s.push_str(&ty);
1152 }
1153 s.push(']');
1154 } else {
1155 s.push_str(&ty);
1156 s.push_str("[]");
1157 }
1158}
1159
1160fn intersection_part_is_union(ty: &DataType) -> bool {
1161 match ty {
1162 DataType::Nullable(_) => true,
1163 DataType::Enum(enm) => enm.variants.len() > 1,
1164 DataType::Intersection(parts) if parts.len() == 1 => intersection_part_is_union(&parts[0]),
1165 _ => false,
1166 }
1167}
1168
1169fn push_nullable(s: &mut String, inner: &str) {
1170 s.push_str(inner);
1171 if inner != NULL && !inner.ends_with(" | null") {
1172 s.push_str(" | null");
1173 }
1174}
1175
1176fn is_exhaustive_map_key(
1177 dt: &DataType,
1178 types: &Types,
1179 generics: &[(GenericReference, DataType)],
1180) -> bool {
1181 match dt {
1182 DataType::Enum(e) => e.variants.iter().filter(|(_, v)| !v.skip).count() == 0,
1183 DataType::Reference(Reference::Named(r)) => named_reference_ty(types, r, &[])
1184 .and_then(|ty| {
1185 named_reference_generics(r).map(|reference_generics| {
1186 let substitution = generics
1187 .iter()
1188 .map(|(generic, dt)| (generic.name().clone(), dt.clone()))
1189 .collect::<GenericSubst>();
1190 let reference_generics = reference_generics
1191 .iter()
1192 .map(|(generic, dt)| {
1193 let mut dt = dt.clone();
1194 let _ = substitute_generics(&mut dt, &substitution);
1195 (generic.clone(), dt)
1196 })
1197 .collect::<Vec<_>>();
1198 is_exhaustive_map_key(ty, types, &reference_generics)
1199 })
1200 })
1201 .unwrap_or(false),
1202 DataType::Struct(strct) => match &strct.fields {
1203 Fields::Unnamed(fields) => {
1204 let mut fields = fields.fields.iter().filter_map(|field| field.ty.as_ref());
1205 fields
1206 .next()
1207 .is_none_or(|dt| is_exhaustive_map_key(dt, types, generics))
1208 && fields.next().is_none()
1209 }
1210 _ => true,
1211 },
1212 DataType::Reference(Reference::Opaque(_)) => false,
1213 DataType::Generic(generic) => generics
1214 .iter()
1215 .find(|(candidate, _)| candidate == generic)
1216 .is_none_or(|(_, dt)| {
1217 matches!(dt, DataType::Generic(candidate) if candidate == generic)
1218 || is_exhaustive_map_key(dt, types, generics)
1219 }),
1220 _ => true,
1221 }
1222}
1223
1224fn render_map(
1225 s: &mut String,
1226 exporter: &Exporter,
1227 format: Option<&dyn Format>,
1228 types: &Types,
1229 map: &Map,
1230 location: Vec<Cow<'static, str>>,
1231 parent_name: Option<&str>,
1232 prefix: &str,
1233 generics: &[(GenericReference, DataType)],
1234 value_mode: RenderMode,
1235) -> Result<(), Error> {
1236 let path = map_key_path(&location);
1237 map_keys::validate_map_key(map.key_ty(), types, format!("{path}.<map_key>"))?;
1238
1239 let rendered_key = map_key_render_type(map.key_ty().clone());
1240 let exhaustive = is_exhaustive_map_key(&rendered_key, types, generics);
1241
1242 if !exhaustive {
1244 s.push_str("Partial<");
1245 }
1246
1247 s.push_str("{ [key in ");
1248 map_key_datatype(
1249 s,
1250 exporter,
1251 format,
1252 types,
1253 &rendered_key,
1254 location.clone(),
1255 parent_name,
1256 prefix,
1257 generics,
1258 )?;
1259 s.push_str("]: ");
1260 value_mode.render(
1261 s,
1262 RenderCtx {
1263 exporter,
1264 format,
1265 types,
1266 parent_name,
1267 prefix,
1268 generics,
1269 },
1270 map.value_ty(),
1271 location,
1272 )?;
1273 s.push_str(" }");
1274
1275 if !exhaustive {
1276 s.push('>');
1277 }
1278
1279 Ok(())
1280}
1281
1282fn shallow_inline_datatype(
1283 s: &mut String,
1284 exporter: &Exporter,
1285 format: Option<&dyn Format>,
1286 types: &Types,
1287 dt: &DataType,
1288 location: Vec<Cow<'static, str>>,
1289 parent_name: Option<&str>,
1290 prefix: &str,
1291 generics: &[(GenericReference, DataType)],
1292) -> Result<(), Error> {
1293 render_datatype(
1294 s,
1295 RenderCtx {
1296 exporter,
1297 format,
1298 types,
1299 parent_name,
1300 prefix,
1301 generics,
1302 },
1303 dt,
1304 location,
1305 RenderMode::ShallowInline,
1306 )
1307}
1308
1309fn intersection_dt(
1310 s: &mut String,
1311 exporter: &Exporter,
1312 format: Option<&dyn Format>,
1313 types: &Types,
1314 parts: &[DataType],
1315 location: Vec<Cow<'static, str>>,
1316 parent_name: Option<&str>,
1317 prefix: &str,
1318 generics: &[(GenericReference, DataType)],
1319 mode: RenderMode,
1320) -> Result<(), Error> {
1321 let mut rendered = Vec::with_capacity(parts.len());
1322 for part in parts {
1323 let mut out = String::new();
1324 mode.render_intersection_part(
1325 &mut out,
1326 RenderCtx {
1327 exporter,
1328 format,
1329 types,
1330 parent_name,
1331 prefix,
1332 generics,
1333 },
1334 part,
1335 location.clone(),
1336 )?;
1337 rendered.push(format!("({out})"));
1338 }
1339
1340 s.push_str(&rendered.join(" & "));
1341 Ok(())
1342}
1343
1344fn inline_datatype(
1346 s: &mut String,
1347 exporter: &Exporter,
1348 format: Option<&dyn Format>,
1349 types: &Types,
1350 dt: &DataType,
1351 location: Vec<Cow<'static, str>>,
1352 parent_name: Option<&str>,
1353 prefix: &str,
1354 depth: usize,
1355 generics: &[(GenericReference, DataType)],
1356) -> Result<(), Error> {
1357 if depth == 25 {
1359 return Err(Error::inline_recursion_limit_exceeded(path_string(
1360 &location,
1361 )));
1362 }
1363
1364 match dt {
1365 DataType::Primitive(p) => s.push_str(primitive_dt(p, location)?),
1366 DataType::Generic(g) => write_generic_reference(s, g),
1367 DataType::List(l) => {
1368 let mut dt_str = String::new();
1369 datatype(
1370 &mut dt_str,
1371 exporter,
1372 format,
1373 types,
1374 &l.ty,
1375 location.clone(),
1376 parent_name,
1377 prefix,
1378 generics,
1379 )?;
1380 push_list(s, &dt_str, l.length);
1381 }
1382 DataType::Map(m) => map_dt(s, exporter, format, types, m, location, generics)?,
1383 DataType::Nullable(def) => {
1384 let mut inner = String::new();
1385 inline_datatype(
1386 &mut inner,
1387 exporter,
1388 format,
1389 types,
1390 def,
1391 location,
1392 parent_name,
1393 "",
1394 depth + 1,
1395 generics,
1396 )?;
1397
1398 push_nullable(s, &inner);
1399 }
1400 DataType::Struct(st) => {
1401 if !generics.is_empty() {
1402 inline_struct_with_generics(
1403 s,
1404 exporter,
1405 format,
1406 types,
1407 st,
1408 location,
1409 parent_name,
1410 prefix,
1411 depth,
1412 generics,
1413 )?;
1414 } else {
1415 struct_dt(
1416 s,
1417 exporter,
1418 format,
1419 types,
1420 st,
1421 location,
1422 parent_name,
1423 prefix,
1424 generics,
1425 )?;
1426 }
1427 }
1428 DataType::Enum(e) => enum_dt(s, exporter, types, e, location, prefix, generics)?,
1429 DataType::Tuple(t) => tuple_dt(s, exporter, types, t, location, generics)?,
1430 DataType::Intersection(types_) => intersection_dt(
1431 s,
1432 exporter,
1433 format,
1434 types,
1435 types_,
1436 location,
1437 parent_name,
1438 prefix,
1439 generics,
1440 RenderMode::Normal,
1441 )?,
1442 DataType::Reference(r) => {
1443 if let Reference::Named(r) = r
1444 && let Ok(ty) = named_reference_ty(types, r, &location)
1445 {
1446 let reference_generics = named_reference_generics(r)?;
1447 let inline_path = path_string(&location);
1448 inline_datatype(
1449 s,
1450 exporter,
1451 format,
1452 types,
1453 ty,
1454 location,
1455 parent_name,
1456 prefix,
1457 depth + 1,
1458 reference_generics,
1459 )
1460 .map_err(|err| err.with_inline_trace(types.get(r), inline_path))?;
1461 } else {
1462 reference_dt(s, exporter, format, types, r, location, prefix, generics)?;
1463 }
1464 }
1465 }
1466
1467 Ok(())
1468}
1469
1470fn inline_struct_with_generics(
1471 s: &mut String,
1472 exporter: &Exporter,
1473 format: Option<&dyn Format>,
1474 types: &Types,
1475 st: &Struct,
1476 location: Vec<Cow<'static, str>>,
1477 parent_name: Option<&str>,
1478 prefix: &str,
1479 depth: usize,
1480 generics: &[(GenericReference, DataType)],
1481) -> Result<(), Error> {
1482 match &st.fields {
1483 Fields::Unit => s.push_str(NULL),
1484 Fields::Unnamed(_) => struct_dt(
1485 s,
1486 exporter,
1487 format,
1488 types,
1489 st,
1490 location,
1491 parent_name,
1492 prefix,
1493 generics,
1494 )?,
1495 Fields::Named(named) => {
1496 s.push('{');
1497 let mut has_field = false;
1498
1499 for (key, field) in &named.fields {
1500 let Some(field_ty) = field.ty.as_ref() else {
1501 continue;
1502 };
1503
1504 has_field = true;
1505 s.push('\n');
1506 s.push_str(prefix);
1507 s.push('\t');
1508 s.push_str(&sanitise_key(key.clone(), false));
1509 if field.optional {
1510 s.push('?');
1511 }
1512 s.push_str(": ");
1513 inline_datatype(
1514 s,
1515 exporter,
1516 format,
1517 types,
1518 field_ty,
1519 location.clone(),
1520 parent_name,
1521 prefix,
1522 depth + 1,
1523 generics,
1524 )?;
1525 s.push(',');
1526 }
1527
1528 if has_field {
1529 s.push('\n');
1530 s.push_str(prefix);
1531 }
1532
1533 s.push('}');
1534 }
1535 }
1536
1537 Ok(())
1538}
1539
1540pub(crate) fn datatype(
1541 s: &mut String,
1542 exporter: &Exporter,
1543 format: Option<&dyn Format>,
1544 types: &Types,
1545 dt: &DataType,
1546 location: Vec<Cow<'static, str>>,
1547 parent_name: Option<&str>,
1548 prefix: &str,
1549 generics: &[(GenericReference, DataType)],
1550) -> Result<(), Error> {
1551 render_datatype(
1552 s,
1553 RenderCtx {
1554 exporter,
1555 format,
1556 types,
1557 parent_name,
1558 prefix,
1559 generics,
1560 },
1561 dt,
1562 location,
1563 RenderMode::Normal,
1564 )
1565}
1566
1567fn primitive_dt(p: &Primitive, location: Vec<Cow<'static, str>>) -> Result<&'static str, Error> {
1568 use Primitive::*;
1569
1570 Ok(match p {
1571 i8 | i16 | i32 | u8 | u16 | u32 => "number",
1572 f16 | f32 | f64 => "number | null",
1574 usize | isize | i64 | u64 | i128 | u128 | f128 => {
1575 return Err(Error::bigint_forbidden(location.join(".")));
1576 }
1577 Primitive::bool => "boolean",
1578 str | char => "string",
1579 })
1580}
1581
1582fn list_dt(
1583 s: &mut String,
1584 exporter: &Exporter,
1585 types: &Types,
1586 l: &List,
1587 location: Vec<Cow<'static, str>>,
1588 generics: &[(GenericReference, DataType)],
1589) -> Result<(), Error> {
1590 let mut dt = String::new();
1591 datatype(
1592 &mut dt, exporter, None, types, &l.ty, location, None, "", generics,
1593 )?;
1594 push_list(s, &dt, l.length);
1595
1596 Ok(())
1597}
1598
1599fn map_key_datatype(
1600 s: &mut String,
1601 exporter: &Exporter,
1602 format: Option<&dyn Format>,
1603 types: &Types,
1604 key_ty: &DataType,
1605 location: Vec<Cow<'static, str>>,
1606 parent_name: Option<&str>,
1607 prefix: &str,
1608 generics: &[(GenericReference, DataType)],
1609) -> Result<(), Error> {
1610 match key_ty {
1611 DataType::Reference(r) => {
1612 reference_dt(s, exporter, format, types, r, location, prefix, generics)
1613 }
1614 key_ty => shallow_inline_datatype(
1615 s,
1616 exporter,
1617 format,
1618 types,
1619 key_ty,
1620 location,
1621 parent_name,
1622 prefix,
1623 generics,
1624 ),
1625 }
1626}
1627
1628fn map_dt(
1629 s: &mut String,
1630 exporter: &Exporter,
1631 format: Option<&dyn Format>,
1632 types: &Types,
1633 m: &Map,
1634 location: Vec<Cow<'static, str>>,
1635 generics: &[(GenericReference, DataType)],
1636) -> Result<(), Error> {
1637 render_map(
1638 s,
1639 exporter,
1640 format,
1641 types,
1642 m,
1643 location,
1644 None,
1645 "",
1646 generics,
1647 RenderMode::Normal,
1648 )
1649}
1650
1651fn map_key_path(location: &[Cow<'static, str>]) -> String {
1652 if location.is_empty() {
1653 return "HashMap".to_string();
1654 }
1655
1656 location.join(".")
1657}
1658
1659fn map_key_render_type(dt: DataType) -> DataType {
1660 if matches!(dt, DataType::Primitive(Primitive::bool)) {
1661 return bool_key_literal_datatype();
1662 }
1663
1664 dt
1665}
1666
1667fn bool_key_literal_datatype() -> DataType {
1668 let mut bool_enum = Enum::default();
1669 bool_enum
1670 .variants
1671 .push((Cow::Borrowed("true"), Variant::unit()));
1672 bool_enum
1673 .variants
1674 .push((Cow::Borrowed("false"), Variant::unit()));
1675 DataType::Enum(bool_enum)
1676}
1677
1678fn unnamed_fields_datatype(
1679 s: &mut String,
1680 exporter: &Exporter,
1681 format: Option<&dyn Format>,
1682 types: &Types,
1683 fields: &[(&Field, &DataType)],
1684 declared_len: usize,
1685 location: Vec<Cow<'static, str>>,
1686 parent_name: Option<&str>,
1687 prefix: &str,
1688 generics: &[(GenericReference, DataType)],
1689 force_inline: bool,
1690) -> Result<(), Error> {
1691 match fields {
1692 [(field, ty)] if declared_len == 1 => {
1697 let mut v = String::new();
1698 datatype_with_inline_attr(
1699 &mut v,
1700 exporter,
1701 format,
1702 types,
1703 ty,
1704 location,
1705 parent_name,
1706 "",
1707 generics,
1708 force_inline,
1709 )?;
1710 s.push_str(&inner_comments(
1711 field.deprecated.as_ref(),
1712 &field.docs,
1713 v,
1714 true,
1715 prefix,
1716 ));
1717 }
1718 fields => {
1719 let optional_from = trailing_optional_run(fields.iter().map(|(field, _)| *field));
1720
1721 s.push('[');
1722 for (i, (field, ty)) in fields.iter().enumerate() {
1723 if i != 0 {
1724 s.push_str(", ");
1725 }
1726
1727 let mut v = String::new();
1728 let mut field_location = location.clone();
1729 field_location.push(i.to_string().into());
1730 datatype_with_inline_attr(
1731 &mut v,
1732 exporter,
1733 format,
1734 types,
1735 ty,
1736 field_location,
1737 parent_name,
1738 "",
1739 generics,
1740 force_inline,
1741 )?;
1742 if i >= optional_from {
1743 if optional_element_needs_parens(ty) {
1744 v = format!("({v})");
1745 }
1746 v.push('?');
1747 }
1748 s.push_str(&inner_comments(
1749 field.deprecated.as_ref(),
1750 &field.docs,
1751 v,
1752 true,
1753 prefix,
1754 ));
1755 }
1756 s.push(']');
1757 }
1758 }
1759
1760 Ok(())
1761}
1762
1763fn trailing_optional_run<'a>(fields: impl Iterator<Item = &'a Field>) -> usize {
1772 let mut run_start = 0;
1773 for (idx, field) in fields.enumerate() {
1774 if !field.optional {
1775 run_start = idx + 1;
1776 }
1777 }
1778 run_start
1779}
1780
1781fn optional_element_needs_parens(ty: &DataType) -> bool {
1785 match ty {
1786 DataType::Nullable(_) | DataType::Enum(_) | DataType::Intersection(_) => true,
1787 DataType::Reference(Reference::Named(reference)) => match &reference.inner {
1788 NamedReferenceType::Inline { dt, .. } => optional_element_needs_parens(dt),
1789 _ => false,
1790 },
1791 DataType::Struct(strct) => match &strct.fields {
1793 Fields::Unnamed(unnamed) if unnamed.fields.len() == 1 => unnamed
1794 .fields
1795 .first()
1796 .and_then(|field| field.ty.as_ref())
1797 .is_some_and(optional_element_needs_parens),
1798 _ => false,
1799 },
1800 _ => false,
1801 }
1802}
1803
1804fn struct_dt(
1805 s: &mut String,
1806 exporter: &Exporter,
1807 format: Option<&dyn Format>,
1808 types: &Types,
1809 st: &Struct,
1810 location: Vec<Cow<'static, str>>,
1811 parent_name: Option<&str>,
1812 prefix: &str,
1813 generics: &[(GenericReference, DataType)],
1814) -> Result<(), Error> {
1815 match &st.fields {
1816 Fields::Unit => s.push_str(NULL),
1817 Fields::Unnamed(unnamed) => unnamed_fields_datatype(
1818 s,
1819 exporter,
1820 format,
1821 types,
1822 &unnamed
1823 .fields
1824 .iter()
1825 .filter_map(|field| field.ty.as_ref().map(|ty| (field, ty)))
1826 .collect::<Vec<_>>(),
1827 unnamed.fields.len(),
1828 location,
1829 parent_name,
1830 prefix,
1831 generics,
1832 false,
1833 )?,
1834 Fields::Named(named) => {
1835 let fields = named
1836 .fields
1837 .iter()
1838 .filter_map(|(name, field)| field.ty.as_ref().map(|ty| (name, (field, ty))))
1839 .collect::<Vec<_>>();
1840
1841 if fields.is_empty() {
1842 s.push_str("Record<string, never>");
1843 return Ok(());
1844 }
1845
1846 let mut unflattened_fields = Vec::with_capacity(fields.len());
1847 for (key, (field, ty)) in fields {
1848 let field_prefix = format!("{prefix}\t");
1849 let mut other = String::new();
1850 let mut field_location = location.clone();
1851 field_location.push(key.clone());
1852 object_field_to_ts(
1853 &mut other,
1854 exporter,
1855 format,
1856 types,
1857 key.clone(),
1858 (field, ty),
1859 field_location,
1860 parent_name,
1861 generics,
1862 &field_prefix,
1863 false,
1864 None,
1865 )?;
1866
1867 unflattened_fields.push(inner_comments(
1868 field.deprecated.as_ref(),
1869 &field.docs,
1870 other,
1871 false,
1872 &field_prefix,
1873 ));
1874 }
1875
1876 s.push('{');
1877 for field in unflattened_fields {
1878 s.push('\n');
1879 s.push_str(&field);
1880 s.push(',');
1881 }
1882 s.push('\n');
1883 s.push_str(prefix);
1884 s.push('}');
1885 }
1886 }
1887
1888 Ok(())
1889}
1890
1891fn object_field_to_ts(
1892 s: &mut String,
1893 exporter: &Exporter,
1894 format: Option<&dyn Format>,
1895 types: &Types,
1896 key: Cow<'static, str>,
1897 (field, ty): (&Field, &DataType),
1898 location: Vec<Cow<'static, str>>,
1899 parent_name: Option<&str>,
1900 generics: &[(GenericReference, DataType)],
1901 prefix: &str,
1902 force_inline: bool,
1903 ty_override: Option<&str>,
1904) -> Result<(), Error> {
1905 let field_name_safe = sanitise_key(key, false);
1906 let key = if field.optional {
1907 format!("{field_name_safe}?").into()
1908 } else {
1909 field_name_safe
1910 };
1911
1912 let value = match ty_override {
1913 Some(ty_override) => ty_override.to_string(),
1914 None => {
1915 let mut value = String::new();
1916 datatype_with_inline_attr(
1917 &mut value,
1918 exporter,
1919 format,
1920 types,
1921 ty,
1922 location,
1923 parent_name,
1924 prefix,
1925 generics,
1926 force_inline,
1927 )?;
1928 value
1929 }
1930 };
1931
1932 s.push_str(prefix);
1933 s.push_str(&key);
1934 s.push_str(": ");
1935 s.push_str(&value);
1936
1937 Ok(())
1938}
1939
1940struct EnumVariantOutput {
1941 value: String,
1942 strict_keys: Option<BTreeSet<String>>,
1943}
1944
1945#[derive(Debug, Clone)]
1946struct DiscriminatorAnalysis {
1947 key: String,
1948 known_literals: Vec<String>,
1949 fallback_variant_idx: Option<usize>,
1950}
1951
1952#[derive(Debug, Clone, Copy)]
1953struct VariantTypeOverride<'a> {
1954 key: &'a str,
1955 ty: &'a str,
1956}
1957
1958#[derive(Debug, Clone)]
1959enum DiscriminatorValue {
1960 StringLiteral(String),
1961 String,
1962}
1963
1964fn analyze_discriminator(
1965 variants: &[&(Cow<'static, str>, Variant)],
1966) -> Option<DiscriminatorAnalysis> {
1967 if variants.iter().any(|(name, _)| name.is_empty()) {
1968 return None;
1969 }
1970
1971 let mut key = None::<String>;
1972 let mut known_literals = BTreeSet::new();
1973 let mut fallback_variant_idx = None;
1974
1975 for (idx, (_, variant)) in variants.iter().enumerate() {
1976 let (variant_key, value) = variant_discriminator(variant)?;
1977
1978 if let Some(expected) = &key {
1979 if expected != &variant_key {
1980 return None;
1981 }
1982 } else {
1983 key = Some(variant_key.clone());
1984 }
1985
1986 match value {
1987 DiscriminatorValue::StringLiteral(value) => {
1988 known_literals.insert(value);
1989 }
1990 DiscriminatorValue::String => {
1991 if fallback_variant_idx.replace(idx).is_some() {
1992 return None;
1993 }
1994 }
1995 }
1996 }
1997
1998 if known_literals.is_empty() {
1999 return None;
2000 }
2001
2002 Some(DiscriminatorAnalysis {
2003 key: key?,
2004 known_literals: known_literals.into_iter().collect(),
2005 fallback_variant_idx,
2006 })
2007}
2008
2009fn variant_discriminator(variant: &Variant) -> Option<(String, DiscriminatorValue)> {
2010 let Fields::Named(named) = &variant.fields else {
2011 return None;
2012 };
2013
2014 let (name, field) = named.fields.iter().find(|(_, field)| !field.optional)?;
2015 let ty = field.ty.as_ref()?;
2016
2017 if matches!(ty, DataType::Primitive(Primitive::str)) {
2018 return Some((name.to_string(), DiscriminatorValue::String));
2019 }
2020
2021 string_literal_datatype_value(ty)
2022 .map(|value| (name.to_string(), DiscriminatorValue::StringLiteral(value)))
2023}
2024
2025fn string_literal_datatype_value(ty: &DataType) -> Option<String> {
2026 let DataType::Enum(enm) = ty else {
2027 return None;
2028 };
2029
2030 let mut variants = enm.variants.iter();
2031 let (name, variant) = variants.next()?;
2032 if variants.next().is_some() || !matches!(&variant.fields, Fields::Unit) {
2033 return None;
2034 }
2035
2036 Some(name.to_string())
2037}
2038
2039fn exclude_known_literals_type(literals: &[String]) -> Option<String> {
2040 if literals.is_empty() {
2041 return None;
2042 }
2043
2044 let known = literals
2045 .iter()
2046 .map(|value| format!("\"{}\"", escape_typescript_string_literal(value.as_str())))
2047 .collect::<Vec<_>>()
2048 .join(" | ");
2049
2050 Some(format!("Exclude<string, {known}>"))
2051}
2052
2053fn fallback_discriminator_override(
2054 discriminator: Option<&DiscriminatorAnalysis>,
2055) -> Option<(usize, &str, String)> {
2056 let discriminator = discriminator?;
2057 discriminator.fallback_variant_idx.and_then(|idx| {
2058 exclude_known_literals_type(&discriminator.known_literals)
2059 .map(|ty| (idx, discriminator.key.as_str(), ty))
2060 })
2061}
2062
2063fn untagged_strict_keys(variant: &Variant) -> Option<BTreeSet<String>> {
2064 match &variant.fields {
2065 Fields::Named(obj) => Some(
2066 obj.fields
2067 .iter()
2068 .filter_map(|(name, field)| {
2069 field
2070 .ty
2071 .as_ref()
2072 .map(|_| sanitise_key(name.clone(), false).to_string())
2073 })
2074 .collect(),
2075 ),
2076 _ => None,
2077 }
2078}
2079
2080fn has_anonymous_variant(variants: &[&(Cow<'static, str>, Variant)]) -> bool {
2081 variants.iter().any(|(name, _)| name.is_empty())
2082}
2083
2084fn active_variants(e: &Enum) -> Vec<&(Cow<'static, str>, Variant)> {
2085 e.variants
2086 .iter()
2087 .filter(|(_, variant)| !variant.skip)
2088 .collect()
2089}
2090
2091fn strictify_enum_variants(variants: &mut [EnumVariantOutput]) {
2092 let strict_key_universe = variants
2093 .iter()
2094 .filter_map(|variant| variant.strict_keys.as_ref())
2095 .flat_map(|keys| keys.iter().cloned())
2096 .collect::<BTreeSet<_>>();
2097
2098 if strict_key_universe.len() < 2 {
2099 return;
2100 }
2101
2102 for variant in variants {
2103 let Some(keys) = variant.strict_keys.as_ref() else {
2104 continue;
2105 };
2106
2107 let missing_keys = strict_key_universe
2108 .iter()
2109 .filter(|key| !keys.contains(*key))
2110 .map(|key| format!("{key}?: {NEVER}"))
2111 .collect::<Vec<_>>();
2112
2113 if !missing_keys.is_empty() {
2114 variant.value = format!("({}) & {{ {} }}", variant.value, missing_keys.join("; "));
2115 }
2116 }
2117}
2118
2119fn push_union(s: &mut String, variants: Vec<String>) {
2120 let mut seen = BTreeSet::new();
2121 let variants = variants
2122 .into_iter()
2123 .filter(|variant| seen.insert(variant.clone()))
2124 .collect::<Vec<_>>();
2125
2126 if variants.is_empty() {
2127 s.push_str(NEVER);
2128 } else {
2129 s.push_str(&variants.join(" | "));
2130 }
2131}
2132
2133fn enum_variant_datatype(
2134 exporter: &Exporter,
2135 format: Option<&dyn Format>,
2136 types: &Types,
2137 name: Cow<'static, str>,
2138 variant: &Variant,
2139 location: Vec<Cow<'static, str>>,
2140 prefix: &str,
2141 generics: &[(GenericReference, DataType)],
2142 ty_override: Option<VariantTypeOverride<'_>>,
2143) -> Result<Option<String>, Error> {
2144 match &variant.fields {
2145 Fields::Unit if name.is_empty() => Err(Error::unsupported_anonymous_enum_variant(
2146 path_string(&location),
2147 "unit",
2148 )),
2149 Fields::Unit => Ok(Some(sanitise_key(name, true).to_string())),
2150 Fields::Named(_) if name.is_empty() => Err(Error::unsupported_anonymous_enum_variant(
2151 path_string(&location),
2152 "named-field",
2153 )),
2154 Fields::Named(obj) => {
2155 let mut regular_fields = Vec::new();
2156 for (field_name, field) in &obj.fields {
2157 let Some(ty) = field.ty.as_ref() else {
2158 continue;
2159 };
2160
2161 let mut other = String::new();
2162 let mut field_location = location.clone();
2163 if field_location
2164 .last()
2165 .is_some_and(|location| location == field_name)
2166 {
2167 if !matches!(ty, DataType::Struct(_)) {
2168 field_location.push("0".into());
2169 }
2170 } else {
2171 field_location.push(field_name.clone());
2172 }
2173 object_field_to_ts(
2174 &mut other,
2175 exporter,
2176 format,
2177 types,
2178 field_name.clone(),
2179 (field, ty),
2180 field_location,
2181 None,
2182 generics,
2183 "",
2184 false,
2185 ty_override
2186 .as_ref()
2187 .filter(|override_ty| override_ty.key == field_name.as_ref())
2188 .map(|override_ty| override_ty.ty),
2189 )?;
2190
2191 regular_fields.push(inner_comments(
2192 field.deprecated.as_ref(),
2193 &field.docs,
2194 other,
2195 true,
2196 prefix,
2197 ));
2198 }
2199
2200 Ok(Some(if regular_fields.is_empty() {
2201 format!("Record<{STRING}, {NEVER}>")
2202 } else {
2203 format!("{{ {} }}", regular_fields.join("; "))
2204 }))
2205 }
2206 Fields::Unnamed(obj) => {
2207 let live_fields = obj
2208 .fields
2209 .iter()
2210 .filter_map(|field| field.ty.as_ref().map(|ty| (field, ty)))
2211 .collect::<Vec<_>>();
2212 let is_newtype = obj.fields.len() == 1;
2215 let optional_from = if is_newtype {
2216 live_fields.len()
2217 } else {
2218 trailing_optional_run(live_fields.iter().map(|(field, _)| *field))
2219 };
2220
2221 let fields = live_fields
2222 .iter()
2223 .enumerate()
2224 .map(|(idx, (_, ty))| {
2225 let mut out = String::new();
2226 let mut field_location = location.clone();
2227 field_location.push(idx.to_string().into());
2228 datatype_with_inline_attr(
2229 &mut out,
2230 exporter,
2231 format,
2232 types,
2233 ty,
2234 field_location,
2235 None,
2236 "",
2237 generics,
2238 false,
2239 )?;
2240 if idx >= optional_from {
2241 if optional_element_needs_parens(ty) {
2242 out = format!("({out})");
2243 }
2244 out.push('?');
2245 }
2246 Ok(out)
2247 })
2248 .collect::<Result<Vec<_>, Error>>()?;
2249
2250 Ok(match &fields[..] {
2251 [] if obj.fields.is_empty() => Some("[]".to_string()),
2252 [] => None,
2253 [field] if is_newtype => Some(field.to_string()),
2254 fields => Some(format!("[{}]", fields.join(", "))),
2255 })
2256 }
2257 }
2258}
2259
2260type AliasId = (Cow<'static, str>, Cow<'static, str>);
2271
2272fn alias_id(ndt: &NamedDataType) -> AliasId {
2273 (ndt.module_path.clone(), ndt.name.clone())
2274}
2275
2276type GenericSubst = HashMap<Cow<'static, str>, DataType>;
2284
2285struct TransparentHop<'t, 'r> {
2297 target: &'t NamedDataType,
2298 reference: &'r NamedReference,
2302 nullable: bool,
2307}
2308
2309fn transparent_reference<'t, 'r>(
2317 types: &'t Types,
2318 dt: &'r DataType,
2319) -> Option<TransparentHop<'t, 'r>> {
2320 match dt {
2321 DataType::Reference(Reference::Named(r)) => match &r.inner {
2322 NamedReferenceType::Reference { .. } => Some(TransparentHop {
2323 target: types.get(r)?,
2324 reference: r,
2325 nullable: false,
2326 }),
2327 _ => None,
2328 },
2329 DataType::Nullable(inner) => Some(TransparentHop {
2330 nullable: true,
2331 ..transparent_reference(types, inner)?
2332 }),
2333 _ => None,
2334 }
2335}
2336
2337fn transparent_variant_hop<'t, 'r>(
2347 types: &'t Types,
2348 variant: &'r Variant,
2349) -> Option<TransparentHop<'t, 'r>> {
2350 let Fields::Unnamed(unnamed) = &variant.fields else {
2351 return None;
2352 };
2353 let [field] = unnamed.fields.as_slice() else {
2354 return None;
2355 };
2356 transparent_reference(types, field.ty.as_ref()?)
2357}
2358
2359fn transparent_export_position(ndt: &NamedDataType) -> Option<&DataType> {
2368 match ndt.ty.as_ref()? {
2369 DataType::Struct(s) => {
2370 let Fields::Unnamed(unnamed) = &s.fields else {
2371 return None;
2372 };
2373 let mut live = unnamed.fields.iter().filter_map(|field| field.ty.as_ref());
2374 let ty = live.next()?;
2375 if live.next().is_some() {
2376 return None;
2377 }
2378 Some(ty)
2379 }
2380 DataType::Enum(_) => None,
2382 dt => Some(dt),
2383 }
2384}
2385
2386fn transparent_export_hop<'t, 'r>(
2389 types: &'t Types,
2390 ndt: &'r NamedDataType,
2391) -> Option<TransparentHop<'t, 'r>> {
2392 transparent_reference(types, transparent_export_position(ndt)?)
2393}
2394
2395struct AliasNode<'a> {
2397 ndt: &'a NamedDataType,
2398 substs: Vec<GenericSubst>,
2404 edges: Vec<AliasId>,
2408}
2409
2410const MAX_SUBSTS_PER_NODE: usize = 1024;
2423const MAX_SUBST_WEIGHT: usize = 256;
2424
2425fn enqueue_cycle_instantiation(
2430 nodes: &mut HashMap<AliasId, AliasNode<'_>>,
2431 work: &mut Vec<(AliasId, GenericSubst)>,
2432 target_id: AliasId,
2433 subst: GenericSubst,
2434) -> Result<(), Cow<'static, str>> {
2435 let node = nodes
2436 .get_mut(&target_id)
2437 .expect("cycle members were discovered in phase 1");
2438 if node.substs.contains(&subst) {
2439 return Ok(());
2440 }
2441 if node.substs.len() >= MAX_SUBSTS_PER_NODE {
2442 return Err(Cow::Borrowed(
2443 "the cycle's generic parameter instantiations do not converge (a single type is instantiated with an unbounded number of distinct arguments)",
2444 ));
2445 }
2446 if subst.values().map(datatype_weight).sum::<usize>() > MAX_SUBST_WEIGHT {
2447 return Err(Cow::Borrowed(
2448 "the cycle's generic parameter instantiations do not converge (a type argument keeps growing with every trip around the cycle)",
2449 ));
2450 }
2451 node.substs.push(subst.clone());
2452 work.push((target_id, subst));
2453 Ok(())
2454}
2455
2456fn node_enum(ndt: &NamedDataType) -> Option<&Enum> {
2457 match ndt.ty.as_ref() {
2458 Some(DataType::Enum(e)) => Some(e),
2459 _ => None,
2460 }
2461}
2462
2463struct EffectiveHop<'t> {
2467 target: &'t NamedDataType,
2468 target_subst: GenericSubst,
2471 nullable: bool,
2472}
2473
2474fn effective_variant_hop<'t>(
2484 types: &'t Types,
2485 variant: &Variant,
2486 subst: &GenericSubst,
2487 identity: &GenericSubst,
2488) -> Result<(Variant, Option<EffectiveHop<'t>>), Cow<'static, str>> {
2489 let raw_hop = transparent_variant_hop(types, variant);
2490
2491 let mut variant = variant.clone();
2492 substitute_fields_generics(&mut variant.fields, subst)?;
2493
2494 if let Some(hop) = raw_hop {
2495 let target_subst = edge_substitution(subst, hop.target, hop.reference)?;
2496 let hop = EffectiveHop {
2497 target: hop.target,
2498 target_subst,
2499 nullable: hop.nullable,
2500 };
2501 return Ok((variant, Some(hop)));
2502 }
2503
2504 let hop = match transparent_variant_hop(types, &variant) {
2505 Some(hop) => Some(EffectiveHop {
2506 target: hop.target,
2507 target_subst: edge_substitution(identity, hop.target, hop.reference)?,
2508 nullable: hop.nullable,
2509 }),
2510 None => None,
2511 };
2512 Ok((variant, hop))
2513}
2514
2515fn effective_export_hop<'t>(
2520 types: &'t Types,
2521 ndt: &NamedDataType,
2522 subst: &GenericSubst,
2523 identity: &GenericSubst,
2524) -> Result<Option<(DataType, Option<EffectiveHop<'t>>)>, Cow<'static, str>> {
2525 let Some(position) = transparent_export_position(ndt) else {
2526 return Ok(None);
2527 };
2528 let raw_hop = transparent_reference(types, position);
2529
2530 let mut position = position.clone();
2531 substitute_generics(&mut position, subst)?;
2532
2533 if let Some(hop) = raw_hop {
2534 let target_subst = edge_substitution(subst, hop.target, hop.reference)?;
2535 let hop = EffectiveHop {
2536 target: hop.target,
2537 target_subst,
2538 nullable: hop.nullable,
2539 };
2540 return Ok(Some((position, Some(hop))));
2541 }
2542
2543 let hop = match transparent_reference(types, &position) {
2544 Some(hop) => Some(EffectiveHop {
2545 target: hop.target,
2546 target_subst: edge_substitution(identity, hop.target, hop.reference)?,
2547 nullable: hop.nullable,
2548 }),
2549 None => None,
2550 };
2551 Ok(Some((position, hop)))
2552}
2553
2554fn edge_substitution(
2564 source_subst: &GenericSubst,
2565 target: &NamedDataType,
2566 reference: &NamedReference,
2567) -> Result<GenericSubst, Cow<'static, str>> {
2568 let (resolved, _, _) = resolved_reference_generics(target, reference, &[]).ok_or(
2569 Cow::Borrowed("the cycle contains a reference whose generic arguments cannot be resolved"),
2570 )?;
2571
2572 let mut scope = source_subst.clone();
2573 let mut target_subst = GenericSubst::with_capacity(resolved.len());
2574 for (generic, mut value) in target.generics.iter().zip(resolved) {
2575 substitute_generics(&mut value, &scope)?;
2576 scope.insert(generic.name.clone(), value.clone());
2577 target_subst.insert(generic.name.clone(), value);
2578 }
2579 Ok(target_subst)
2580}
2581
2582fn subst_is_identity(subst: &GenericSubst) -> bool {
2587 subst
2588 .iter()
2589 .all(|(name, dt)| matches!(dt, DataType::Generic(g) if g.name() == name))
2590}
2591
2592fn substitute_generics(dt: &mut DataType, subst: &GenericSubst) -> Result<(), Cow<'static, str>> {
2599 match dt {
2600 DataType::Generic(g) => match subst.get(g.name()) {
2601 Some(replacement) => {
2602 *dt = replacement.clone();
2603 Ok(())
2604 }
2605 None => Err(Cow::Owned(format!(
2606 "the cycle references generic parameter `{}` with no instantiation for it",
2607 g.name()
2608 ))),
2609 },
2610 DataType::Primitive(_) => Ok(()),
2611 DataType::List(list) => substitute_generics(&mut list.ty, subst),
2612 DataType::Map(map) => {
2613 substitute_generics(map.key_ty_mut(), subst)?;
2614 substitute_generics(map.value_ty_mut(), subst)
2615 }
2616 DataType::Struct(s) => substitute_fields_generics(&mut s.fields, subst),
2617 DataType::Enum(e) => e
2618 .variants
2619 .iter_mut()
2620 .try_for_each(|(_, variant)| substitute_fields_generics(&mut variant.fields, subst)),
2621 DataType::Tuple(t) => t
2622 .elements
2623 .iter_mut()
2624 .try_for_each(|ty| substitute_generics(ty, subst)),
2625 DataType::Nullable(inner) => substitute_generics(inner, subst),
2626 DataType::Intersection(parts) => parts
2627 .iter_mut()
2628 .try_for_each(|ty| substitute_generics(ty, subst)),
2629 DataType::Reference(Reference::Named(r)) => match &mut r.inner {
2630 NamedReferenceType::Reference { generics, .. } => generics
2631 .iter_mut()
2632 .try_for_each(|(_, dt)| substitute_generics(dt, subst)),
2633 NamedReferenceType::Inline { dt, .. } => substitute_generics(dt, subst),
2634 NamedReferenceType::Recursive(_) => Ok(()),
2635 },
2636 DataType::Reference(Reference::Opaque(opaque_ref)) => {
2637 if let Some(branded) = opaque_ref.downcast_ref::<Branded>() {
2638 let mut ty = branded.ty().clone();
2642 let brand = branded.brand().clone();
2643 substitute_generics(&mut ty, subst)?;
2644 *dt = DataType::Reference(Reference::opaque(Branded::new(brand, ty)));
2645 Ok(())
2646 } else if subst_is_identity(subst) {
2647 Ok(())
2650 } else if let Some(def) = opaque_ref.downcast_ref::<opaque::Define>() {
2651 match subst.iter().find(|(name, replacement)| {
2658 !matches!(replacement, DataType::Generic(g) if g.name() == *name)
2659 && raw_ts_mentions_identifier(&def.0, name)
2660 }) {
2661 Some((name, _)) => Err(Cow::Owned(format!(
2662 "the cycle contains raw TypeScript (`define(...)`) that mentions generic parameter `{name}`, which cannot be rewritten"
2663 ))),
2664 None => Ok(()),
2665 }
2666 } else if opaque_ref.downcast_ref::<opaque::Any>().is_some()
2667 || opaque_ref.downcast_ref::<opaque::Unknown>().is_some()
2668 || opaque_ref.downcast_ref::<opaque::Never>().is_some()
2669 || opaque_ref.downcast_ref::<opaque::Number>().is_some()
2670 || opaque_ref.downcast_ref::<opaque::BigInt>().is_some()
2671 {
2672 Ok(())
2675 } else {
2676 Err(Cow::Owned(format!(
2679 "the cycle contains an opaque reference (`{}`) whose payload cannot be checked for generic parameters",
2680 opaque_ref.type_name()
2681 )))
2682 }
2683 }
2684 }
2685}
2686
2687fn raw_ts_mentions_identifier(raw: &str, name: &str) -> bool {
2696 fn is_ident_byte(b: u8) -> bool {
2697 b.is_ascii_alphanumeric() || b == b'_' || b == b'$' || !b.is_ascii()
2698 }
2699
2700 if name.is_empty() {
2701 return false;
2702 }
2703 let bytes = raw.as_bytes();
2704 let mut search_start = 0;
2705 while let Some(pos) = raw[search_start..].find(name) {
2706 let start = search_start + pos;
2707 let end = start + name.len();
2708 if (start == 0 || !is_ident_byte(bytes[start - 1]))
2709 && (end == raw.len() || !is_ident_byte(bytes[end]))
2710 {
2711 return true;
2712 }
2713 search_start = end;
2718 }
2719 false
2720}
2721
2722fn datatype_weight(dt: &DataType) -> usize {
2727 fn fields_weight(fields: &Fields) -> usize {
2728 match fields {
2729 Fields::Unit => 1,
2730 Fields::Unnamed(unnamed) => unnamed
2731 .fields
2732 .iter()
2733 .filter_map(|field| field.ty.as_ref())
2734 .map(datatype_weight)
2735 .sum(),
2736 Fields::Named(named) => named
2737 .fields
2738 .iter()
2739 .filter_map(|(_, field)| field.ty.as_ref())
2740 .map(datatype_weight)
2741 .sum(),
2742 }
2743 }
2744
2745 1_usize.saturating_add(match dt {
2746 DataType::Primitive(_) | DataType::Generic(_) => 0,
2747 DataType::List(list) => datatype_weight(&list.ty),
2748 DataType::Map(map) => {
2749 datatype_weight(map.key_ty()).saturating_add(datatype_weight(map.value_ty()))
2750 }
2751 DataType::Struct(s) => fields_weight(&s.fields),
2752 DataType::Enum(e) => e
2753 .variants
2754 .iter()
2755 .map(|(_, variant)| fields_weight(&variant.fields))
2756 .sum(),
2757 DataType::Tuple(t) => t.elements.iter().map(datatype_weight).sum(),
2758 DataType::Nullable(inner) => datatype_weight(inner),
2759 DataType::Intersection(parts) => parts.iter().map(datatype_weight).sum(),
2760 DataType::Reference(Reference::Named(r)) => match &r.inner {
2761 NamedReferenceType::Reference { generics, .. } => {
2762 generics.iter().map(|(_, dt)| datatype_weight(dt)).sum()
2763 }
2764 NamedReferenceType::Inline { dt, .. } => datatype_weight(dt),
2765 NamedReferenceType::Recursive(_) => 0,
2766 },
2767 DataType::Reference(Reference::Opaque(opaque_ref)) => opaque_ref
2768 .downcast_ref::<Branded>()
2769 .map(|branded| datatype_weight(branded.ty()))
2770 .unwrap_or(0),
2771 })
2772}
2773
2774fn substitute_fields_generics(
2775 fields: &mut Fields,
2776 subst: &GenericSubst,
2777) -> Result<(), Cow<'static, str>> {
2778 match fields {
2779 Fields::Unit => Ok(()),
2780 Fields::Unnamed(unnamed) => unnamed
2781 .fields
2782 .iter_mut()
2783 .filter_map(|field| field.ty.as_mut())
2784 .try_for_each(|ty| substitute_generics(ty, subst)),
2785 Fields::Named(named) => named
2786 .fields
2787 .iter_mut()
2788 .filter_map(|(_, field)| field.ty.as_mut())
2789 .try_for_each(|ty| substitute_generics(ty, subst)),
2790 }
2791}
2792
2793fn collapse_untagged_alias_cycle(
2817 types: &Types,
2818 root: &NamedDataType,
2819) -> Result<Option<Enum>, Cow<'static, str>> {
2820 if node_enum(root).is_none() {
2825 return Ok(None);
2826 }
2827 let root_id = alias_id(root);
2828
2829 let identity: GenericSubst = root
2830 .generics
2831 .iter()
2832 .map(|g| (g.name.clone(), DataType::Generic(g.reference())))
2833 .collect();
2834
2835 let mut nodes: HashMap<AliasId, AliasNode<'_>> = HashMap::new();
2852 nodes.insert(
2853 root_id.clone(),
2854 AliasNode {
2855 ndt: root,
2856 substs: vec![identity.clone()],
2857 edges: Vec::new(),
2858 },
2859 );
2860
2861 let mut work: Vec<(AliasId, GenericSubst)> = vec![(root_id.clone(), identity.clone())];
2862 let mut cursor = 0;
2863 while cursor < work.len() {
2864 let (id, subst) = work[cursor].clone();
2865 cursor += 1;
2866
2867 let ndt = nodes[&id].ndt;
2868 let mut hops: Vec<(&NamedDataType, Option<GenericSubst>)> = Vec::new();
2869 match node_enum(ndt) {
2870 Some(enm) => {
2871 for (_, variant) in &enm.variants {
2872 if variant.skip {
2873 continue;
2874 }
2875 match effective_variant_hop(types, variant, &subst, &identity) {
2876 Ok((_, Some(hop))) => hops.push((hop.target, Some(hop.target_subst))),
2877 Ok((_, None)) => {}
2878 Err(_) => {
2879 if let Some(hop) = transparent_variant_hop(types, variant) {
2880 hops.push((hop.target, None));
2881 }
2882 }
2883 }
2884 }
2885 }
2886 None => match effective_export_hop(types, ndt, &subst, &identity) {
2887 Ok(Some((_, Some(hop)))) => hops.push((hop.target, Some(hop.target_subst))),
2888 Ok(_) => {}
2889 Err(_) => {
2890 if let Some(hop) = transparent_export_hop(types, ndt) {
2891 hops.push((hop.target, None));
2892 }
2893 }
2894 },
2895 }
2896
2897 for (target, target_subst) in hops {
2898 let target_id = alias_id(target);
2899 if let Entry::Vacant(entry) = nodes.entry(target_id.clone()) {
2900 entry.insert(AliasNode {
2901 ndt: target,
2902 substs: Vec::new(),
2903 edges: Vec::new(),
2904 });
2905 }
2906
2907 let source = nodes
2908 .get_mut(&id)
2909 .expect("node was inserted before being visited");
2910 if !source.edges.contains(&target_id) {
2911 source.edges.push(target_id.clone());
2912 }
2913
2914 let target_subst = match target_subst {
2918 Some(target_subst) => target_subst,
2919 None => target
2920 .generics
2921 .iter()
2922 .map(|g| (g.name.clone(), DataType::Generic(g.reference())))
2923 .collect(),
2924 };
2925 enqueue_cycle_instantiation(&mut nodes, &mut work, target_id, target_subst)?;
2926 }
2927 }
2928
2929 let mut reverse: HashMap<&AliasId, Vec<&AliasId>> = HashMap::new();
2935 for (id, node) in &nodes {
2936 for target in &node.edges {
2937 reverse.entry(target).or_default().push(id);
2938 }
2939 }
2940 let mut in_cycle: HashSet<AliasId> = HashSet::new();
2941 let mut frontier = vec![&root_id];
2942 while let Some(id) = frontier.pop() {
2943 for &source in reverse.get(id).into_iter().flatten() {
2944 if in_cycle.insert(source.clone()) {
2945 frontier.push(source);
2946 }
2947 }
2948 }
2949 if !in_cycle.contains(&root_id) {
2950 return Ok(None);
2951 }
2952 drop(frontier);
2953 drop(reverse);
2954
2955 for node in nodes.values_mut() {
2980 node.substs.clear();
2981 }
2982 let mut work: Vec<(AliasId, GenericSubst)> = vec![(root_id.clone(), identity.clone())];
2983 nodes
2984 .get_mut(&root_id)
2985 .expect("root node always exists")
2986 .substs
2987 .push(identity.clone());
2988
2989 let mut variants = Vec::new();
2990 let mut needs_null = false;
2991 let mut cursor = 0;
2992 while cursor < work.len() {
2993 let (id, subst) = work[cursor].clone();
2994 cursor += 1;
2995
2996 let ndt = nodes[&id].ndt;
2997 let Some(enm) = node_enum(ndt) else {
2998 let Some((position, hop)) = effective_export_hop(types, ndt, &subst, &identity)? else {
3005 continue;
3006 };
3007 match hop {
3008 Some(hop) if in_cycle.contains(&alias_id(hop.target)) => {
3009 needs_null |= hop.nullable;
3010 enqueue_cycle_instantiation(
3011 &mut nodes,
3012 &mut work,
3013 alias_id(hop.target),
3014 hop.target_subst,
3015 )?;
3016 }
3017 _ => variants.push((
3018 Cow::Borrowed("passthrough"),
3019 Variant::unnamed().field(Field::new(position)).build(),
3020 )),
3021 }
3022 continue;
3023 };
3024
3025 for (name, variant) in &enm.variants {
3026 if variant.skip {
3027 continue;
3028 }
3029
3030 let (variant, hop) = effective_variant_hop(types, variant, &subst, &identity)?;
3031 if let Some(hop) = hop
3032 && in_cycle.contains(&alias_id(hop.target))
3033 {
3034 needs_null |= hop.nullable;
3035 enqueue_cycle_instantiation(
3036 &mut nodes,
3037 &mut work,
3038 alias_id(hop.target),
3039 hop.target_subst,
3040 )?;
3041 continue;
3042 }
3043
3044 variants.push((name.clone(), variant));
3045 }
3046 }
3047
3048 if needs_null {
3049 variants.push((
3052 Cow::Borrowed("null"),
3053 Variant::unnamed()
3054 .field(Field::new(DataType::Tuple(Tuple::new(vec![]))))
3055 .build(),
3056 ));
3057 }
3058
3059 let mut collapsed = Enum::default();
3060 collapsed.variants = variants;
3061 Ok(Some(collapsed))
3062}
3063
3064fn resolve_named_export_body<'a>(
3069 types: &Types,
3070 ndt: &'a NamedDataType,
3071) -> Result<Cow<'a, DataType>, Error> {
3072 let ty = ndt.ty.as_ref().expect("named datatype must have a body");
3073 match collapse_untagged_alias_cycle(types, ndt) {
3074 Ok(Some(collapsed)) => Ok(Cow::Owned(DataType::Enum(collapsed))),
3075 Ok(None) => Ok(Cow::Borrowed(ty)),
3076 Err(reason) => Err(Error::unrepresentable_alias_cycle(
3077 rust_type_path(ndt).into_owned(),
3078 reason,
3079 )
3080 .with_named_datatype(ndt)),
3081 }
3082}
3083
3084fn enum_dt(
3085 s: &mut String,
3086 exporter: &Exporter,
3087 types: &Types,
3088 e: &Enum,
3089 location: Vec<Cow<'static, str>>,
3090 prefix: &str,
3091 generics: &[(GenericReference, DataType)],
3092) -> Result<(), Error> {
3093 let is_finite_number = e.attributes.get_named_as("specta:finite_number") == Some(&true);
3094
3095 if e.attributes.get_named_as(FIELD_ALIAS_UNION_MARKER) == Some(&true) {
3096 return alias_field_union_dt(s, exporter, types, e, location, prefix, generics);
3097 }
3098
3099 if e.variants.is_empty() {
3100 s.push_str(NEVER);
3101 return Ok(());
3102 }
3103
3104 let filtered_variants = active_variants(e);
3105
3106 let discriminator = analyze_discriminator(&filtered_variants);
3107 let fallback_override = fallback_discriminator_override(discriminator.as_ref());
3108
3109 let mut rendered_variants = Vec::with_capacity(filtered_variants.len());
3110 for (idx, (variant_name, variant)) in filtered_variants.iter().enumerate() {
3111 let variant_override = fallback_override
3112 .as_ref()
3113 .and_then(|(fallback_idx, key, ty)| {
3114 (*fallback_idx == idx).then_some(VariantTypeOverride {
3115 key,
3116 ty: ty.as_str(),
3117 })
3118 });
3119
3120 let mut variant_location = location.clone();
3121 variant_location.push(variant_name.clone());
3122 let ts_values = enum_variant_datatype(
3123 exporter,
3124 None,
3125 types,
3126 variant_name.clone(),
3127 variant,
3128 variant_location,
3129 prefix,
3130 generics,
3131 variant_override,
3132 )?;
3133
3134 rendered_variants.push(EnumVariantOutput {
3135 value: ts_values.unwrap_or_else(|| NEVER.to_string()),
3136 strict_keys: untagged_strict_keys(variant),
3137 });
3138 }
3139
3140 if is_finite_number {
3142 s.push_str("number");
3143 return Ok(());
3144 }
3145
3146 if discriminator.is_none() && !has_anonymous_variant(&filtered_variants) {
3147 strictify_enum_variants(&mut rendered_variants);
3148 }
3149
3150 let variants = filtered_variants
3151 .into_iter()
3152 .zip(rendered_variants)
3153 .map(|((_, variant), rendered)| {
3154 inner_comments(
3155 variant.deprecated.as_ref(),
3156 &variant.docs,
3157 rendered.value,
3158 true,
3159 prefix,
3160 )
3161 })
3162 .collect::<Vec<_>>();
3163
3164 push_union(s, variants);
3165
3166 Ok(())
3167}
3168
3169fn alias_field_union_dt(
3179 s: &mut String,
3180 exporter: &Exporter,
3181 types: &Types,
3182 e: &Enum,
3183 location: Vec<Cow<'static, str>>,
3184 prefix: &str,
3185 generics: &[(GenericReference, DataType)],
3186) -> Result<(), Error> {
3187 let mut fields = Vec::with_capacity(e.variants.len());
3188 let mut all_optional = true;
3189
3190 for (name, variant) in active_variants(e) {
3191 let mut variant = (*variant).clone();
3192 if let Fields::Unnamed(unnamed) = &mut variant.fields
3193 && let Some(DataType::Struct(strct)) = unnamed
3194 .fields
3195 .first_mut()
3196 .and_then(|field| field.ty.as_mut())
3197 && let Fields::Named(named) = &mut strct.fields
3198 {
3199 named
3200 .fields
3201 .retain(|(_, field)| !field.attributes.contains_key(FIELD_ALIAS_EXCLUSION_MARKER));
3202 all_optional &= named.fields.iter().all(|(_, field)| field.optional);
3203 } else {
3204 all_optional = false;
3205 }
3206
3207 let mut variant_location = location.clone();
3208 variant_location.push(name.clone());
3209 let field = enum_variant_datatype(
3210 exporter,
3211 None,
3212 types,
3213 name.clone(),
3214 &variant,
3215 variant_location,
3216 prefix,
3217 generics,
3218 None,
3219 )?
3220 .unwrap_or_else(|| NEVER.to_string());
3221 fields.push(field);
3222 }
3223
3224 let source = fields.join(" & ");
3225 if all_optional {
3226 s.push_str(&source);
3227 return Ok(());
3228 }
3229
3230 s.push_str("((");
3231 s.push_str(&source);
3232 s.push_str(") extends infer T extends object ? { [K in keyof T]: { [P in keyof T as P extends K ? P : never]: T[P] } & { [P in keyof T as P extends K ? never : P]?: never } }[keyof T] & object : never)");
3233
3234 Ok(())
3235}
3236
3237fn tuple_dt(
3238 s: &mut String,
3239 exporter: &Exporter,
3240 types: &Types,
3241 t: &Tuple,
3242 location: Vec<Cow<'static, str>>,
3243 generics: &[(GenericReference, DataType)],
3244) -> Result<(), Error> {
3245 match t.elements.as_slice() {
3246 [] => s.push_str(NULL),
3247 elements => {
3248 s.push('[');
3249 for (idx, dt) in elements.iter().enumerate() {
3250 if idx != 0 {
3251 s.push_str(", ");
3252 }
3253 let mut element_location = location.clone();
3254 element_location.push(idx.to_string().into());
3255 datatype(
3256 s,
3257 exporter,
3258 None,
3259 types,
3260 dt,
3261 element_location,
3262 None,
3263 "",
3264 generics,
3265 )?;
3266 }
3267 s.push(']');
3268 }
3269 }
3270
3271 Ok(())
3272}
3273
3274fn reference_dt(
3275 s: &mut String,
3276 exporter: &Exporter,
3277 format: Option<&dyn Format>,
3278 types: &Types,
3279 r: &Reference,
3280 location: Vec<Cow<'static, str>>,
3281 prefix: &str,
3282 generics: &[(GenericReference, DataType)],
3283) -> Result<(), Error> {
3284 match r {
3285 Reference::Named(r) => match &r.inner {
3286 NamedReferenceType::Reference { .. } => {
3287 reference_named_dt(s, exporter, types, r, location, generics)
3288 }
3289 NamedReferenceType::Inline { dt, .. } => {
3290 let inline_path = path_string(&location);
3291 inline_datatype(
3292 s, exporter, format, types, dt, location, None, prefix, 0, generics,
3293 )
3294 .map_err(|err| err.with_inline_trace(types.get(r), inline_path))
3295 }
3296 NamedReferenceType::Recursive(cycle) => Err(Error::infinite_recursive_inline_type(
3297 path_string(&location),
3298 format!("{r:?}"),
3299 cycle.clone(),
3300 )),
3301 },
3302 Reference::Opaque(r) => reference_opaque_dt(s, exporter, format, types, r, location),
3303 }
3304}
3305
3306fn reference_opaque_dt(
3307 s: &mut String,
3308 exporter: &Exporter,
3309 format: Option<&dyn Format>,
3310 types: &Types,
3311 r: &OpaqueReference,
3312 location: Vec<Cow<'static, str>>,
3313) -> Result<(), Error> {
3314 if let Some(def) = r.downcast_ref::<opaque::Define>() {
3315 s.push_str(&def.0);
3316 return Ok(());
3317 }
3318
3319 if r.downcast_ref::<opaque::Any>().is_some() {
3320 s.push_str("any");
3321 return Ok(());
3322 }
3323
3324 if r.downcast_ref::<opaque::Unknown>().is_some() {
3325 s.push_str("unknown");
3326 return Ok(());
3327 }
3328
3329 if r.downcast_ref::<opaque::Never>().is_some() {
3330 s.push_str("never");
3331 return Ok(());
3332 }
3333
3334 if r.downcast_ref::<opaque::Number>().is_some() {
3335 s.push_str("number");
3336 return Ok(());
3337 }
3338
3339 if r.downcast_ref::<opaque::BigInt>().is_some() {
3340 s.push_str("bigint");
3341 return Ok(());
3342 }
3343
3344 if let Some(def) = r.downcast_ref::<Branded>() {
3345 if let Some(branded_type) = exporter
3346 .branded_type_impl
3347 .as_ref()
3348 .map(|builder| {
3349 (builder.0)(
3350 BrandedTypeExporter {
3351 exporter,
3352 format,
3353 types,
3354 },
3355 def,
3356 )
3357 })
3358 .transpose()?
3359 {
3360 s.push_str(branded_type.as_ref());
3361 return Ok(());
3362 }
3363
3364 match def.ty() {
3365 DataType::Reference(r) => {
3366 reference_dt(s, exporter, format, types, r, location.clone(), "", &[])?
3367 }
3368 ty => inline_datatype(
3369 s,
3370 exporter,
3371 format,
3372 types,
3373 ty,
3374 location.clone(),
3375 None,
3376 "",
3377 0,
3378 &[],
3379 )?,
3380 }
3381 s.push_str(r#" & { readonly __brand: ""#);
3382 s.push_str(&escape_typescript_string_literal(def.brand()));
3383 s.push_str("\" }");
3384 return Ok(());
3385 }
3386
3387 Err(Error::unsupported_opaque_reference(
3388 path_string(&location),
3389 r.clone(),
3390 ))
3391}
3392
3393fn reference_named_dt(
3394 s: &mut String,
3395 exporter: &Exporter,
3396 types: &Types,
3397 r: &NamedReference,
3398 location: Vec<Cow<'static, str>>,
3399 generics: &[(GenericReference, DataType)],
3400) -> Result<(), Error> {
3401 let path = path_string(&location);
3402 let ndt = types
3403 .get(r)
3404 .ok_or_else(|| Error::dangling_named_reference(path.clone(), format!("{r:?}")))?;
3405 crate::references::track_nr(r);
3407
3408 let name = referenced_type_name(exporter, ndt);
3409
3410 let (rendered_generics, omit_generics, scoped_generics) =
3411 resolved_reference_generics(ndt, r, generics)
3412 .ok_or_else(|| Error::dangling_named_reference(path, format!("{r:?}")))?;
3413
3414 s.push_str(&name);
3415 if !omit_generics && !rendered_generics.is_empty() {
3416 s.push('<');
3417
3418 for (i, dt) in rendered_generics.iter().enumerate() {
3419 if i != 0 {
3420 s.push_str(", ");
3421 }
3422
3423 datatype(
3424 s,
3425 exporter,
3426 None,
3427 types,
3428 dt,
3429 vec![],
3430 None,
3431 "",
3432 &scoped_generics,
3433 )?;
3434 }
3435
3436 s.push('>');
3437 }
3438
3439 Ok(())
3440}