1use std::borrow::Cow;
11use std::collections::{BTreeMap, BTreeSet};
12use std::fmt::Write;
13
14use crate::docs_view::{DocAvailability, Unscoped, project_declaration};
15use crate::runtime::prelude::declaration::prelude_package_declaration;
16use crate::stdlib::Stdlib;
17use crate::types::escape_string_literal;
18use crate::{
19 ClassExtends, DocCapabilityBindingKind, DocCapabilityLiteral, DocComment, FileId,
20 NamespaceSymbol, PackageDeclaration, Param, Span, Type, TypeKind, TypePredicate, TypeSymbol,
21 ValueKind, ValueSymbol,
22};
23
24const INTERNAL_MODULE: &str = "submilli:security";
26
27pub struct ModuleDoc {
30 pub name: String,
31 pub description: String,
32 pub declarations: String,
33}
34
35pub struct ModuleSummary {
37 pub name: String,
38 pub description: String,
39}
40
41pub fn docs(name: &str) -> Option<ModuleDoc> {
45 Stdlib::core().docs(name)
46}
47
48pub fn search(query: &str) -> Vec<ModuleSummary> {
51 Stdlib::core().search(query)
52}
53
54impl Stdlib {
57 pub fn view(self, availability: &dyn DocAvailability) -> StdlibView {
61 self.view_where(availability, |_, usable| usable)
62 }
63
64 pub fn view_where(
68 self,
69 availability: &dyn DocAvailability,
70 keep: impl Fn(&str, bool) -> bool,
71 ) -> StdlibView {
72 let modules = user_modules(self)
73 .iter()
74 .filter_map(|defs| {
75 let projection = project_declaration(defs, availability);
76 keep(&defs.package_name, projection.usable).then_some(projection.declaration)
77 })
78 .collect();
79 StdlibView { modules }
80 }
81
82 pub fn docs(self, name: &str) -> Option<ModuleDoc> {
84 self.view(&Unscoped).docs(name)
85 }
86
87 pub fn search(self, query: &str) -> Vec<ModuleSummary> {
89 self.view(&Unscoped).search(query)
90 }
91
92 pub fn resolve(self, name: &str) -> Resolution {
94 self.view(&Unscoped).resolve(name)
95 }
96
97 pub fn suggest_filtered(
99 self,
100 name: &str,
101 extra: &[String],
102 visible: impl Fn(&str) -> bool,
103 ) -> Option<String> {
104 self.view(&Unscoped).suggest_filtered(name, extra, visible)
105 }
106
107 pub fn catalog_filtered(
109 self,
110 extra: Vec<CatalogEntry>,
111 visible: impl Fn(&str) -> bool,
112 ) -> Catalog {
113 self.view(&Unscoped).catalog_filtered(extra, visible)
114 }
115
116 pub fn render_stdlib_d_ts(self) -> String {
118 let mut out = String::new();
119 for defs in &editor_stdlib_modules(self) {
120 render_declare_module(&mut out, defs);
121 }
122 out.trim_end().to_string()
123 }
124
125 pub fn render_packages_d_ts(
127 self,
128 packages: &[&PackageDeclaration],
129 context: &[&PackageDeclaration],
130 ) -> String {
131 let stdlib = editor_stdlib_modules(self);
132 let exporters = TypeExporters::new(
133 stdlib
134 .iter()
135 .chain(packages.iter().copied())
136 .chain(context.iter().copied()),
137 );
138 let globals = global_classes();
139 let mut out = String::new();
140 for defs in packages {
141 let defs = with_public_type_names(defs);
142 let defs = defs.as_ref();
143 let _ = writeln!(out, "declare module \"{}\" {{", defs.package_name);
144 let imports = block_imports(defs, &exporters);
145 for (local, (public, module)) in &imports.types {
146 if local == public {
147 let _ = writeln!(out, " import type {{ {local} }} from \"{module}\";");
148 } else {
149 let _ = writeln!(
150 out,
151 " import type {{ {public} as {local} }} from \"{module}\";"
152 );
153 }
154 }
155 let parent = |extends: &ClassExtends| {
156 if let Some(local) = imports.parents.get(extends.parent.as_str()) {
157 return Some(ts_named_type(local, &extends.args));
158 }
159 let global = globals.get(extends.parent.as_str())?;
162 if declares(defs, global, true) {
163 return None;
164 }
165 Some(ts_named_type(global, &extends.args))
166 };
167 render_ts_declarations(&mut out, defs, " ", "export ", &parent);
168 let _ = writeln!(out, "}}\n");
169 }
170 out.trim_end().to_string()
171 }
172}
173
174pub struct StdlibView {
178 modules: Vec<PackageDeclaration>,
179}
180
181impl StdlibView {
182 pub fn names(&self) -> impl Iterator<Item = &str> {
184 self.modules.iter().map(|defs| defs.package_name.as_str())
185 }
186
187 pub fn contains(&self, name: &str) -> bool {
189 self.declaration(name).is_some()
190 }
191
192 pub fn declaration(&self, name: &str) -> Option<&PackageDeclaration> {
194 self.modules.iter().find(|defs| defs.package_name == name)
195 }
196
197 pub fn docs(&self, name: &str) -> Option<ModuleDoc> {
199 self.declaration(name).map(|defs| ModuleDoc {
200 name: defs.package_name.clone(),
201 description: module_description(&defs.package_name).to_string(),
202 declarations: render_declarations(defs),
203 })
204 }
205
206 pub fn search(&self, query: &str) -> Vec<ModuleSummary> {
208 let q = query.trim().to_lowercase();
209 self.modules
210 .iter()
211 .filter(|defs| matches_query(defs, &q))
212 .map(|defs| ModuleSummary {
213 name: defs.package_name.clone(),
214 description: module_description(&defs.package_name).to_string(),
215 })
216 .collect()
217 }
218
219 pub fn resolve(&self, name: &str) -> Resolution {
221 if let Some(doc) = self.docs(name) {
222 return Resolution::Module(doc);
223 }
224 match builtin_lookup(name) {
225 BuiltinLookup::Found(declarations) => Resolution::Builtin {
226 name: name.to_string(),
227 declarations,
228 },
229 BuiltinLookup::UnknownMember {
230 path,
231 member,
232 members,
233 } => Resolution::UnknownMember {
234 path,
235 member,
236 members,
237 },
238 BuiltinLookup::Unknown => Resolution::Unknown,
239 }
240 }
241
242 pub fn suggest_filtered(
244 &self,
245 name: &str,
246 extra: &[String],
247 visible: impl Fn(&str) -> bool,
248 ) -> Option<String> {
249 let mut candidates: Vec<String> = self.search("").into_iter().map(|m| m.name).collect();
250 let builtins = builtins();
251 candidates.extend(builtins.types);
252 candidates.extend(builtins.namespaces);
253 candidates.extend(extra.iter().cloned());
254 candidates.retain(|candidate| visible(candidate));
255
256 let query = name.split('.').find(|s| !s.is_empty()).unwrap_or(name);
259 if query.len() < 3 {
263 return None;
264 }
265
266 if let Some((_, replacement)) = OMITTED_GLOBALS
267 .iter()
268 .find(|(omitted, _)| omitted.eq_ignore_ascii_case(name))
269 {
270 return Some((*replacement).to_string());
271 }
272
273 let is_self = |c: &String| c.eq_ignore_ascii_case(name);
276 if let Some(hit) = candidates
277 .iter()
278 .find(|c| !is_self(c) && name_tail(c).eq_ignore_ascii_case(query))
279 {
280 return Some(hit.clone());
281 }
282 let mut keys: Vec<(&str, &str)> = Vec::new();
286 for c in candidates.iter().filter(|c| !is_self(c)) {
287 keys.push((c.as_str(), c.as_str()));
288 let tail = name_tail(c);
289 if tail != c.as_str() {
290 keys.push((tail, c.as_str()));
291 }
292 }
293 let hit = crate::did_you_mean::closest_match(query, keys.iter().map(|(k, _)| *k))?;
294 keys.iter()
295 .find(|(k, _)| *k == hit)
296 .map(|(_, owner)| (*owner).to_string())
297 }
298
299 pub fn catalog_filtered(
301 &self,
302 extra: Vec<CatalogEntry>,
303 visible: impl Fn(&str) -> bool,
304 ) -> Catalog {
305 let mut entries: Vec<CatalogEntry> = self
306 .search("")
307 .into_iter()
308 .map(|m| CatalogEntry {
309 name: m.name,
310 source: SOURCE_STDLIB.to_string(),
311 description: m.description,
312 })
313 .collect();
314 entries.extend(extra);
315 entries.retain(|entry| visible(&entry.name));
316 let remaining = entries.len().saturating_sub(CATALOG_LIMIT);
317 entries.truncate(CATALOG_LIMIT);
318 Catalog { entries, remaining }
319 }
320}
321
322pub struct Builtins {
327 pub types: Vec<String>,
328 pub namespaces: Vec<String>,
329}
330
331const JSON_BUILTIN: &str = "JSON";
334
335const SUPPORTING_TYPES: &[&str] = &[
339 "Console",
340 "Base64Options",
341 "RegExpMatch",
342 "Iterator",
343 "Iterable",
344 "IteratorResult",
345 "IteratorYieldResult",
346 "IteratorReturnResult",
347];
348
349fn is_headline_type(name: &str) -> bool {
350 !name.contains('#') && !name.ends_with("Constructor") && !SUPPORTING_TYPES.contains(&name)
353}
354
355pub fn builtins() -> Builtins {
357 let defs = builtin_package_declaration();
358 let mut types: Vec<String> = defs
360 .types
361 .keys()
362 .filter(|n| is_headline_type(n))
363 .cloned()
364 .collect();
365 types.push("Record".into());
366 types.sort();
367 let mut namespaces: Vec<String> = defs.namespaces.keys().cloned().collect();
368 namespaces.push(JSON_BUILTIN.to_string());
369 namespaces.sort();
370 Builtins { types, namespaces }
371}
372
373pub fn builtin_docs(name: &str) -> Option<String> {
377 match builtin_lookup(name) {
378 BuiltinLookup::Found(declarations) => Some(declarations),
379 BuiltinLookup::UnknownMember { .. } | BuiltinLookup::Unknown => None,
380 }
381}
382
383#[derive(Debug, Clone, PartialEq, Eq)]
387pub enum BuiltinLookup {
388 Found(String),
390 UnknownMember {
392 path: String,
393 member: String,
394 members: Vec<String>,
395 },
396 Unknown,
398}
399
400pub fn builtin_lookup(name: &str) -> BuiltinLookup {
409 if name.contains('#') {
412 return BuiltinLookup::Unknown;
413 }
414 let segments: Vec<&str> = name.split('.').filter(|s| !s.is_empty()).collect();
417 let Some((head, rest)) = segments.split_first() else {
418 return BuiltinLookup::Unknown;
419 };
420
421 if head.eq_ignore_ascii_case("Record") && rest.is_empty() {
422 return BuiltinLookup::Found("/** Record<K, V> accepts string keys. With K = string, reads return V | undefined and writes require V. Finite string-literal keys are all required. Equivalent open syntax: { [key: string]: V }. */\ntype Record<K extends string, V> = { [P in K]: V };\n".into());
423 }
424 if head.eq_ignore_ascii_case(JSON_BUILTIN) {
425 let defs = json_package_declaration();
426 return walk_namespace(JSON_BUILTIN, &defs.namespaces[JSON_BUILTIN], rest);
427 }
428
429 let defs = builtin_package_declaration();
430 if let Some(canonical) = resolve_ignore_case(defs.namespaces.keys(), head) {
431 return walk_namespace(&canonical, &defs.namespaces[&canonical], rest);
432 }
433
434 let Some(canonical) = resolve_type_key(&defs.types, head) else {
435 return BuiltinLookup::Unknown;
436 };
437 match rest {
438 [] => {
439 let mut out = String::new();
440 render_type_with_ctor(&mut out, &defs.types, &canonical, "");
441 BuiltinLookup::Found(out.trim_end().to_string())
442 }
443 [member, deeper @ ..] => resolve_type_member(&defs.types, &canonical, member, deeper),
444 }
445}
446
447fn walk_namespace<'a>(head: &str, head_ns: &'a NamespaceSymbol, rest: &[&str]) -> BuiltinLookup {
449 let mut chain: Vec<String> = vec![head.to_string()];
450 let mut ns: &'a NamespaceSymbol = head_ns;
451
452 for (idx, seg) in rest.iter().enumerate() {
453 let tail = &rest[idx + 1..];
454
455 if let Some(canonical) = resolve_ignore_case(ns.namespaces.keys(), seg) {
456 let sub = &ns.namespaces[&canonical];
457 if tail.is_empty() {
458 let mut body = String::new();
459 render_namespace(&mut body, &canonical, sub, "");
460 return found_in(&chain, &body);
461 }
462 chain.push(canonical);
463 ns = sub;
464 continue;
465 }
466
467 if let Some(canonical) = resolve_type_key(&ns.types, seg) {
468 if tail.is_empty() {
469 let mut body = String::new();
470 render_namespace_member(&mut body, ns, &canonical);
471 return found_in(&chain, &body);
472 }
473 return match resolve_type_member(&ns.types, &canonical, tail[0], &tail[1..]) {
476 BuiltinLookup::Found(body) => found_in(&chain, &body),
477 miss => qualify(&chain, miss),
478 };
479 }
480
481 if let Some(canonical) = resolve_ignore_case(ns.values.keys(), seg) {
482 if tail.is_empty() {
483 let sym = &ns.values[&canonical];
484 let mut body = String::new();
485 push_doc(&mut body, value_doc(&sym.kind), "");
486 render_value(&mut body, &canonical, &sym.kind, "");
487 return found_in(&chain, &body);
488 }
489 chain.push(canonical);
491 return BuiltinLookup::UnknownMember {
492 path: chain.join("."),
493 member: tail.join("."),
494 members: Vec::new(),
495 };
496 }
497
498 return BuiltinLookup::UnknownMember {
499 path: chain.join("."),
500 member: (*seg).to_string(),
501 members: namespace_member_names(ns),
502 };
503 }
504
505 let mut out = String::new();
506 render_namespace(&mut out, head, head_ns, "");
507 BuiltinLookup::Found(out.trim_end().to_string())
508}
509
510fn resolve_type_member(
513 types: &BTreeMap<String, TypeSymbol>,
514 type_name: &str,
515 member: &str,
516 deeper: &[&str],
517) -> BuiltinLookup {
518 if !deeper.is_empty() {
519 return BuiltinLookup::UnknownMember {
520 path: format!("{type_name}.{member}"),
521 member: deeper.join("."),
522 members: Vec::new(),
523 };
524 }
525 let ctor = format!("{type_name}Constructor");
526 for owner in [type_name, ctor.as_str()] {
527 let Some(sym) = types.get(owner) else {
528 continue;
529 };
530 if let Some(body) = render_type_member(owner, &sym.kind, member) {
531 return BuiltinLookup::Found(body.trim_end().to_string());
532 }
533 }
534 BuiltinLookup::UnknownMember {
535 path: type_name.to_string(),
536 member: member.to_string(),
537 members: type_member_names(types, type_name),
538 }
539}
540
541fn render_namespace_member(out: &mut String, ns: &NamespaceSymbol, canonical: &str) {
549 if let Some(sym) = ns.values.get(canonical) {
550 push_doc(out, value_doc(&sym.kind), "");
551 render_value(out, canonical, &sym.kind, "");
552 }
553 if let Some(sym) = ns.types.get(canonical) {
554 push_doc(out, type_doc(&sym.kind), "");
555 render_type(out, canonical, &sym.kind, "");
556 }
557 let ctor = format!("{canonical}Constructor");
558 if let Some(sym) = ns.types.get(&ctor) {
559 push_doc(out, type_doc(&sym.kind), "");
560 render_type(out, &ctor, &sym.kind, "");
561 }
562}
563
564fn render_type_member(owner: &str, kind: &TypeKind, member: &str) -> Option<String> {
567 if let TypeKind::Class { .. } = kind {
568 return render_class_member(owner, kind, member);
569 }
570 let TypeKind::Interface {
571 generics,
572 methods,
573 properties,
574 ..
575 } = kind
576 else {
577 return None;
578 };
579 let mut body = String::new();
580 if let Some(name) = resolve_ignore_case(properties.keys(), member) {
581 let prop = &properties[&name];
582 push_doc(&mut body, &prop.doc, " ");
583 let ro = if prop.readonly { "readonly " } else { "" };
584 let opt = if prop.optional { "?" } else { "" };
585 let name = member_name(&name);
586 let _ = writeln!(body, " {ro}{name}{opt}: {};", prop.ty);
587 } else {
588 let name = resolve_ignore_case(methods.keys(), member)?;
589 let m = &methods[&name];
590 push_doc(&mut body, &m.doc, " ");
591 let optional = if m.optional { "?" } else { "" };
592 let name = method_prefix(&name);
593 let _ = writeln!(
594 body,
595 " {name}{optional}{}({}): {};",
596 generics_str(&m.generics),
597 params_str(&m.params, m.doc.as_ref()),
598 m.ret
599 );
600 }
601 Some(format!(
602 "interface {owner}{} {{\n{body}}}\n",
603 generics_str(generics)
604 ))
605}
606
607fn render_class_member(owner: &str, kind: &TypeKind, member: &str) -> Option<String> {
611 let TypeKind::Class {
612 generics,
613 fields,
614 methods,
615 method_visibility,
616 statics,
617 static_visibility,
618 static_fields,
619 ..
620 } = kind
621 else {
622 return None;
623 };
624 let mut body = String::new();
625 if let Some(name) = resolve_ignore_case(static_fields.keys(), member)
626 .filter(|n| static_fields[n].visibility != crate::Visibility::Private)
627 {
628 let field = &static_fields[&name];
629 push_doc(&mut body, &field.doc, " ");
630 let ro = if field.readonly { "readonly " } else { "" };
631 let name = member_name(&name);
632 let _ = writeln!(body, " static {ro}{name}: {};", field.ty);
633 } else if let Some(name) = resolve_ignore_case(statics.keys(), member)
634 .filter(|n| static_visibility.get(n) != Some(&crate::Visibility::Private))
635 {
636 let m = &statics[&name];
637 push_doc(&mut body, &m.doc, " ");
638 let name = member_name(&name);
639 let _ = writeln!(
640 body,
641 " static {name}{}({}): {};",
642 generics_str(&m.generics),
643 params_str(&m.params, m.doc.as_ref()),
644 m.ret
645 );
646 } else if let Some(name) = resolve_ignore_case(fields.keys(), member)
647 .filter(|n| fields[n].visibility != crate::Visibility::Private)
648 {
649 let field = &fields[&name];
650 push_doc(&mut body, &field.doc, " ");
651 let ro = if field.readonly { "readonly " } else { "" };
652 let opt = if field.optional { "?" } else { "" };
653 let name = member_name(&name);
654 let _ = writeln!(body, " {ro}{name}{opt}: {};", field.ty);
655 } else {
656 let name = resolve_ignore_case(methods.keys(), member)
657 .filter(|n| method_visibility.get(n) != Some(&crate::Visibility::Private))?;
658 let m = &methods[&name];
659 push_doc(&mut body, &m.doc, " ");
660 let optional = if m.optional { "?" } else { "" };
661 let name = member_name(&name);
662 let _ = writeln!(
663 body,
664 " {name}{optional}{}({}): {};",
665 generics_str(&m.generics),
666 params_str(&m.params, m.doc.as_ref()),
667 m.ret
668 );
669 }
670 Some(format!(
671 "class {owner}{} {{\n{body}}}\n",
672 generics_str(generics)
673 ))
674}
675
676fn type_member_names(types: &BTreeMap<String, TypeSymbol>, type_name: &str) -> Vec<String> {
678 let ctor = format!("{type_name}Constructor");
679 let mut names: Vec<String> =
680 [type_name, ctor.as_str()]
681 .iter()
682 .filter_map(|owner| types.get(*owner))
683 .flat_map(|sym| match &sym.kind {
684 TypeKind::Interface {
685 methods,
686 properties,
687 ..
688 } => properties
689 .keys()
690 .chain(methods.keys())
691 .cloned()
692 .collect::<Vec<_>>(),
693 TypeKind::Class {
696 fields,
697 methods,
698 method_visibility,
699 statics,
700 static_visibility,
701 static_fields,
702 ..
703 } => {
704 static_fields
705 .iter()
706 .filter(|(_, f)| f.visibility != crate::Visibility::Private)
707 .map(|(n, _)| n)
708 .chain(statics.keys().filter(|n| {
709 static_visibility.get(*n) != Some(&crate::Visibility::Private)
710 }))
711 .chain(
712 fields
713 .iter()
714 .filter(|(_, f)| f.visibility != crate::Visibility::Private)
715 .map(|(n, _)| n),
716 )
717 .chain(methods.keys().filter(|n| {
718 method_visibility.get(*n) != Some(&crate::Visibility::Private)
719 }))
720 .cloned()
721 .collect::<Vec<_>>()
722 }
723 _ => Vec::new(),
724 })
725 .collect();
726 names.sort();
727 names.dedup();
728 names
729}
730
731fn namespace_member_names(ns: &NamespaceSymbol) -> Vec<String> {
734 let mut names: Vec<String> = ns
735 .values
736 .keys()
737 .chain(ns.types.keys())
738 .chain(ns.namespaces.keys())
739 .filter(|n| !n.ends_with("Constructor"))
740 .cloned()
741 .collect();
742 names.sort();
743 names.dedup();
744 names
745}
746
747fn resolve_type_key(types: &BTreeMap<String, TypeSymbol>, name: &str) -> Option<String> {
751 resolve_ignore_case(types.keys(), name).or_else(|| {
752 resolve_ignore_case(types.keys(), &format!("{name}Constructor"))
753 .map(|c| c.trim_end_matches("Constructor").to_string())
754 })
755}
756
757fn found_in(chain: &[String], body: &str) -> BuiltinLookup {
759 let mut out = body.trim_end().to_string();
760 for name in chain.iter().rev() {
761 out = format!("namespace {name} {{\n{}\n}}", indent_block(&out, " "));
762 }
763 BuiltinLookup::Found(out)
764}
765
766fn qualify(chain: &[String], miss: BuiltinLookup) -> BuiltinLookup {
768 match miss {
769 BuiltinLookup::UnknownMember {
770 path,
771 member,
772 members,
773 } if !chain.is_empty() => BuiltinLookup::UnknownMember {
774 path: format!("{}.{path}", chain.join(".")),
775 member,
776 members,
777 },
778 other => other,
779 }
780}
781
782pub enum Resolution {
788 Module(ModuleDoc),
790 Builtin { name: String, declarations: String },
792 UnknownMember {
794 path: String,
795 member: String,
796 members: Vec<String>,
797 },
798 Unknown,
800}
801
802pub fn resolve(name: &str) -> Resolution {
805 Stdlib::core().resolve(name)
806}
807
808const OMITTED_GLOBALS: &[(&str, &str)] = &[("Date", "Temporal")];
820
821pub fn suggest(name: &str, extra: &[String]) -> Option<String> {
822 suggest_filtered(name, extra, |_| true)
823}
824
825pub fn suggest_filtered(
827 name: &str,
828 extra: &[String],
829 visible: impl Fn(&str) -> bool,
830) -> Option<String> {
831 Stdlib::core().suggest_filtered(name, extra, visible)
832}
833
834fn name_tail(name: &str) -> &str {
837 let after_scheme = name.rsplit_once(':').map_or(name, |(_, t)| t);
838 after_scheme
839 .rsplit_once('/')
840 .map_or(after_scheme, |(_, t)| t)
841}
842
843#[derive(Debug, Clone, PartialEq, Eq)]
847pub struct CatalogEntry {
848 pub name: String,
849 pub source: String,
850 pub description: String,
851}
852
853pub struct Catalog {
855 pub entries: Vec<CatalogEntry>,
856 pub remaining: usize,
859}
860
861pub const CATALOG_LIMIT: usize = 50;
863
864pub fn builtins_pointer(fetch_with: &str) -> String {
872 let head = "Language built-ins (Array, Map, String, Temporal, JSON, …)";
873 format!("{head} are always in scope without an import — fetch them with {fetch_with}.")
874}
875
876pub fn package_correcting_message(name: &str, fetch_with: &str) -> String {
885 format!(
886 "`{name}` is a package, not a language built-in. {fetch_with} for its declarations, \
887 and write `import ... from \"{name}\"` to use it."
888 )
889}
890
891pub const SOURCE_STDLIB: &str = "stdlib";
893
894pub fn catalog(extra: Vec<CatalogEntry>) -> Catalog {
896 catalog_filtered(extra, |_| true)
897}
898
899pub fn catalog_filtered(extra: Vec<CatalogEntry>, visible: impl Fn(&str) -> bool) -> Catalog {
901 Stdlib::core().catalog_filtered(extra, visible)
902}
903
904pub fn unknown_member_message(path: &str, member: &str, members: &[String]) -> String {
906 if members.is_empty() {
907 return format!("`{path}` has no member `{member}` — `{path}` has no members.");
908 }
909 format!(
910 "`{path}` has no member `{member}`. Available members of `{path}`: {}.",
911 members.join(", ")
912 )
913}
914
915pub fn builtin_no_import_note(name: &str) -> String {
919 format!("`{name}` is a language built-in, always in scope — do not write an `import` for it.")
920}
921
922fn indent_block(body: &str, pad: &str) -> String {
923 body.lines()
924 .map(|l| {
925 if l.is_empty() {
926 String::new()
927 } else {
928 format!("{pad}{l}")
929 }
930 })
931 .collect::<Vec<_>>()
932 .join("\n")
933}
934
935fn builtin_package_declaration() -> PackageDeclaration {
936 let mut defs = prelude_package_declaration();
937 let prelude_decl = crate::runtime::prelude::package_declaration();
938 for (name, namespace) in prelude_decl.namespaces {
939 defs.namespaces.entry(name).or_insert(namespace);
940 }
941 defs
942}
943
944fn resolve_ignore_case<'a>(keys: impl Iterator<Item = &'a String>, name: &str) -> Option<String> {
947 let mut fallback = None;
948 for key in keys {
949 if key == name {
950 return Some(key.clone());
951 }
952 if fallback.is_none() && key.eq_ignore_ascii_case(name) {
953 fallback = Some(key.clone());
954 }
955 }
956 fallback
957}
958
959fn user_modules(stdlib: Stdlib) -> Vec<PackageDeclaration> {
961 stdlib
962 .package_declarations()
963 .into_iter()
964 .filter(|d| d.package_name != INTERNAL_MODULE)
965 .collect()
966}
967
968fn module_description(name: &str) -> &'static str {
971 match name {
972 "submilli:agents" => {
973 "Harness sub-agents: run<T>(agent, input), and list() to discover them."
974 }
975 "submilli:crypto" => "Hashing, HMAC, and random bytes.",
976 "submilli:code" => {
977 "Workspace tools: numbered reads, search, glob, tree, anchored edits and unified diffs."
978 }
979 "submilli:embedding" => {
980 "Remote text embeddings: embed batches into sealed vectors, and models() to discover aliases."
981 }
982 "submilli:fs" => "Sandbox filesystem: files and directories under the VFS root.",
983 "submilli:git" => "Capability-controlled Git repositories in the VFS, with HTTPS remotes.",
984 "submilli:http" => "Outbound HTTP requests, and downloads into the filesystem.",
985 "submilli:llm" => "Gated model calls: call/batch, and models() to discover them.",
986 "submilli:secrets" => "Policy-gated access to Blueprint-declared secrets.",
987 "submilli:session" => {
988 "Session-scoped key-value state and durable package-owned resource cleanup."
989 }
990 "submilli:skills" => "Harness skills: list(), load(name) and readFile(name, path).",
991 "submilli:url" => "URL parse/build and query-string handling. Pure compute.",
992 "submilli:variables" => "Immutable Blueprint/session configuration for packages.",
993 "submilli:uuid" => "UUID v4/v7 generation and validation.",
994 _ => "",
995 }
996}
997
998fn matches_query(defs: &PackageDeclaration, q: &str) -> bool {
999 if q.is_empty() {
1000 return true;
1001 }
1002 let in_symbols = defs
1003 .values
1004 .keys()
1005 .chain(defs.types.keys())
1006 .any(|k| k.to_lowercase().contains(q));
1007 defs.package_name.to_lowercase().contains(q)
1008 || module_description(&defs.package_name)
1009 .to_lowercase()
1010 .contains(q)
1011 || in_symbols
1012}
1013
1014pub fn render_declarations(defs: &PackageDeclaration) -> String {
1018 let defs = with_public_type_names(defs);
1019 let defs = defs.as_ref();
1020 let mut out = String::new();
1021 for (name, sym) in &defs.values {
1022 push_doc(&mut out, value_doc(&sym.kind), "");
1023 render_value(&mut out, name, &sym.kind, "");
1024 }
1025 for (name, sym) in &defs.types {
1026 push_doc(&mut out, type_doc(&sym.kind), "");
1027 render_type(&mut out, name, &sym.kind, "");
1028 }
1029 out.trim_end().to_string()
1030}
1031
1032fn with_public_type_names(defs: &PackageDeclaration) -> Cow<'_, PackageDeclaration> {
1041 let mut public: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
1044 for (name, symbol) in &defs.types {
1045 if symbol.mangled_name != crate::mangle::package_symbol(&defs.package_name, name) {
1046 public
1047 .entry(symbol.mangled_name.as_str())
1048 .or_default()
1049 .push(name.as_str());
1050 }
1051 }
1052 if public.is_empty() {
1053 return Cow::Borrowed(defs);
1054 }
1055 let Ok(mut value) = serde_json::to_value(defs) else {
1056 return Cow::Borrowed(defs);
1057 };
1058 let mut renamed = false;
1059 let mut pending = vec![&mut value];
1060 while let Some(node) = pending.pop() {
1061 match node {
1062 serde_json::Value::Object(fields) => {
1063 let Some(serde_json::Value::String(mangled)) = fields.get("mangled") else {
1064 pending.extend(fields.values_mut());
1065 continue;
1066 };
1067 if let Some(names) = public.get(mangled.as_str())
1070 && let Some(serde_json::Value::String(name)) = fields.get_mut("name")
1071 && !names.contains(&name.as_str())
1072 && let Some(first) = names.first()
1073 {
1074 *name = first.to_string();
1075 renamed = true;
1076 }
1077 pending.extend(fields.get_mut("args"));
1078 }
1079 serde_json::Value::Array(items) => pending.extend(items),
1080 _ => {}
1081 }
1082 }
1083 if !renamed {
1084 return Cow::Borrowed(defs);
1085 }
1086 serde_json::from_value(value).map_or(Cow::Borrowed(defs), Cow::Owned)
1087}
1088
1089pub fn render_stdlib_d_ts() -> String {
1096 Stdlib::core().render_stdlib_d_ts()
1097}
1098
1099pub fn render_packages_d_ts(
1109 packages: &[&PackageDeclaration],
1110 context: &[&PackageDeclaration],
1111) -> String {
1112 Stdlib::core().render_packages_d_ts(packages, context)
1113}
1114
1115fn global_classes() -> BTreeMap<String, String> {
1118 let defs = builtin_package_declaration();
1119 defs.types
1120 .into_iter()
1121 .filter(|(name, symbol)| {
1122 matches!(symbol.kind, TypeKind::Class { .. })
1123 && !is_hidden_prelude_type(&defs.package_name, name)
1124 })
1125 .map(|(name, symbol)| (symbol.mangled_name.as_str().to_string(), name))
1126 .collect()
1127}
1128
1129fn editor_stdlib_modules(stdlib: Stdlib) -> Vec<PackageDeclaration> {
1130 let mut modules = user_modules(stdlib);
1131 modules.push(crate::stdlib::test::package_declaration());
1132 modules.push(crate::stdlib::security::package_declaration());
1133 modules.sort_by(|a, b| a.package_name.cmp(&b.package_name));
1134 modules
1135}
1136
1137struct TypeExporters<'a> {
1139 by_mangled: BTreeMap<&'a str, (ExportedType<'a>, &'a str)>,
1141 by_module: BTreeMap<&'a str, BTreeMap<&'a str, ExportedType<'a>>>,
1144}
1145
1146#[derive(Clone, Copy)]
1147struct ExportedType<'a> {
1148 name: &'a str,
1149 is_value: bool,
1152}
1153
1154impl<'a> TypeExporters<'a> {
1155 fn new(modules: impl Iterator<Item = &'a PackageDeclaration>) -> Self {
1156 let mut by_mangled = BTreeMap::new();
1157 let mut by_module: BTreeMap<&str, BTreeMap<&str, ExportedType>> = BTreeMap::new();
1158 for defs in modules {
1159 let module = defs.package_name.as_str();
1160 for (name, symbol) in &defs.types {
1161 let exported = ExportedType {
1162 name: name.as_str(),
1163 is_value: matches!(
1164 symbol.kind,
1165 TypeKind::Class { .. }
1166 | TypeKind::NumberEnum { .. }
1167 | TypeKind::StringEnum { .. }
1168 ),
1169 };
1170 by_mangled.insert(symbol.mangled_name.as_str(), (exported, module));
1171 by_module
1172 .entry(module)
1173 .or_default()
1174 .insert(name.as_str(), exported);
1175 }
1176 }
1177 Self {
1178 by_mangled,
1179 by_module,
1180 }
1181 }
1182
1183 fn resolve(&self, reference: &TypeReference) -> Option<(ExportedType<'a>, &'a str)> {
1186 if let Some(&exported) = self.by_mangled.get(reference.mangled.as_str()) {
1187 return Some(exported);
1188 }
1189 let (module, types) = self.by_module.get_key_value(reference.package.as_str())?;
1190 Some((*types.get(reference.name.as_str())?, module))
1191 }
1192}
1193
1194struct TypeReference {
1198 mangled: String,
1199 package: String,
1200 name: String,
1201}
1202
1203struct BlockImports<'a> {
1205 types: BTreeMap<String, (&'a str, &'a str)>,
1208 parents: BTreeMap<&'a str, String>,
1211}
1212
1213fn block_imports<'a>(
1226 defs: &'a PackageDeclaration,
1227 exporters: &TypeExporters<'a>,
1228) -> BlockImports<'a> {
1229 let mut types = BTreeMap::new();
1230 for reference in rendered_type_references(defs) {
1231 let Some((exported, module)) = exporters.resolve(&reference) else {
1232 continue;
1233 };
1234 if module != defs.package_name && !declares(defs, &reference.name, exported.is_value) {
1235 types
1236 .entry(reference.name)
1237 .or_insert((exported.name, module));
1238 }
1239 }
1240 let mut parents = BTreeMap::new();
1241 for (mangled, exported, module) in class_parents(defs, exporters) {
1242 if module == defs.package_name {
1243 parents.insert(mangled, exported.name.to_string());
1244 continue;
1245 }
1246 let already_imported = types
1247 .iter()
1248 .find(|(_, target)| **target == (exported.name, module))
1249 .map(|(local, _)| local.clone());
1250 let local = if let Some(local) = already_imported {
1251 local
1252 } else {
1253 let local = free_local_name(defs, &types, exported.name);
1254 types.insert(local.clone(), (exported.name, module));
1255 local
1256 };
1257 parents.insert(mangled, local);
1258 }
1259 BlockImports { types, parents }
1260}
1261
1262fn free_local_name(
1266 defs: &PackageDeclaration,
1267 imports: &BTreeMap<String, (&str, &str)>,
1268 public: &str,
1269) -> String {
1270 let is_free = |name: &str| !imports.contains_key(name) && !declares(defs, name, true);
1271 if is_free(public) {
1272 return public.to_string();
1273 }
1274 let taken = imports
1275 .len()
1276 .saturating_add(defs.types.len())
1277 .saturating_add(defs.values.len())
1278 .saturating_add(defs.namespaces.len());
1279 (1..=taken.saturating_add(1))
1280 .map(|suffix| format!("{public}{suffix}"))
1281 .find(|name| is_free(name))
1282 .unwrap_or_else(|| format!("{public}{}", taken.saturating_add(1)))
1283}
1284
1285fn declares(defs: &PackageDeclaration, name: &str, borrowed_is_value: bool) -> bool {
1288 defs.types.contains_key(name)
1289 || (borrowed_is_value
1290 && (defs.values.contains_key(name) || defs.namespaces.contains_key(name)))
1291}
1292
1293fn class_parents<'a>(
1296 defs: &'a PackageDeclaration,
1297 exporters: &TypeExporters<'a>,
1298) -> Vec<(&'a str, ExportedType<'a>, &'a str)> {
1299 defs.types
1300 .values()
1301 .filter_map(|symbol| match &symbol.kind {
1302 TypeKind::Class {
1303 extends: Some(extends),
1304 ..
1305 } => {
1306 let parent = extends.parent.as_str();
1307 let &(exported, module) = exporters.by_mangled.get(parent)?;
1308 Some((parent, exported, module))
1309 }
1310 _ => None,
1311 })
1312 .collect()
1313}
1314
1315fn rendered_type_references(defs: &PackageDeclaration) -> Vec<TypeReference> {
1327 let types: BTreeMap<&String, TypeSymbol> = defs
1328 .types
1329 .iter()
1330 .map(|(name, symbol)| (name, public_surface(symbol)))
1331 .collect();
1332 collect_type_references(&[
1333 serde_json::to_value(&defs.values),
1334 serde_json::to_value(&types),
1335 serde_json::to_value(&defs.namespaces),
1336 ])
1337}
1338
1339pub(crate) fn type_references(
1342 rendered: &[Result<serde_json::Value, serde_json::Error>],
1343) -> Vec<String> {
1344 collect_type_references(rendered)
1345 .into_iter()
1346 .map(|reference| reference.mangled)
1347 .collect()
1348}
1349
1350pub(crate) fn type_symbol_references(symbol: &TypeSymbol) -> Vec<String> {
1354 let mut references = type_references(&[serde_json::to_value(public_surface(symbol))]);
1355 if let TypeKind::Class {
1356 extends,
1357 implements,
1358 ..
1359 } = &symbol.kind
1360 {
1361 references.extend(
1362 extends
1363 .iter()
1364 .map(|extends| extends.parent.as_str().to_string()),
1365 );
1366 references.extend(implements.iter().map(|name| name.as_str().to_string()));
1367 }
1368 references
1369}
1370
1371fn collect_type_references(
1372 rendered: &[Result<serde_json::Value, serde_json::Error>],
1373) -> Vec<TypeReference> {
1374 let mut pending: Vec<&serde_json::Value> = rendered.iter().flatten().collect();
1376 let mut references = Vec::new();
1377 while let Some(value) = pending.pop() {
1378 match value {
1379 serde_json::Value::Object(fields) => {
1380 let Some(serde_json::Value::String(mangled)) = fields.get("mangled") else {
1381 pending.extend(fields.values());
1382 continue;
1383 };
1384 let text = |key: &str| {
1385 fields
1386 .get(key)
1387 .and_then(serde_json::Value::as_str)
1388 .unwrap_or_default()
1389 .to_string()
1390 };
1391 references.push(TypeReference {
1392 mangled: mangled.clone(),
1393 package: text("package"),
1394 name: text("name"),
1395 });
1396 pending.extend(fields.get("args"));
1397 }
1398 serde_json::Value::Array(items) => pending.extend(items),
1399 _ => {}
1400 }
1401 }
1402 references
1403}
1404
1405fn public_surface(symbol: &TypeSymbol) -> TypeSymbol {
1408 let mut symbol = symbol.clone();
1409 if let TypeKind::Class {
1410 fields,
1411 narrowing_checks,
1412 methods,
1413 method_visibility,
1414 accessors,
1415 constructor,
1416 constructor_visibility,
1417 statics,
1418 static_visibility,
1419 static_fields,
1420 ..
1421 } = &mut symbol.kind
1422 {
1423 if *constructor_visibility == crate::Visibility::Private {
1424 constructor.clear();
1425 }
1426 let is_public = |visibility: Option<&crate::Visibility>| {
1427 visibility != Some(&crate::Visibility::Private)
1428 };
1429 accessors.retain(|accessor| is_public(fields.get(accessor.name()).map(|f| &f.visibility)));
1430 fields.retain(|_, field| is_public(Some(&field.visibility)));
1431 static_fields.retain(|_, field| is_public(Some(&field.visibility)));
1432 methods.retain(|name, _| is_public(method_visibility.get(name)));
1433 statics.retain(|name, _| is_public(static_visibility.get(name)));
1434 narrowing_checks.clear();
1435 }
1436 symbol
1437}
1438
1439pub fn render_lib_submilli_d_ts() -> String {
1442 let defs = builtin_package_declaration();
1443 let globals = global_classes();
1444 let parent = |extends: &ClassExtends| {
1445 let global = globals.get(extends.parent.as_str())?;
1446 Some(ts_named_type(global, &extends.args))
1447 };
1448 let mut out = String::new();
1449 render_ts_declarations(&mut out, &defs, "", "declare ", &parent);
1450 render_ts_compiler_globals(&mut out, &defs);
1451 render_ts_declarations(&mut out, &global_functions(), "", "declare ", &|_| None);
1452 let json = json_package_declaration();
1453 render_ts_declarations(&mut out, &json, "", "declare ", &|_| None);
1454 out.trim_end().to_string()
1455}
1456
1457fn global_functions() -> PackageDeclaration {
1461 const NAMES: [&str; 6] = [
1462 "parseInt",
1463 "parseFloat",
1464 "encodeURIComponent",
1465 "encodeURI",
1466 "decodeURIComponent",
1467 "decodeURI",
1468 ];
1469 let mut globals = PackageDeclaration::with_package(crate::mangle::PRELUDE_PACKAGE);
1470 let sources = crate::runtime::host_package_declarations()
1471 .into_iter()
1472 .chain([crate::runtime::prelude::package_declaration()]);
1473 for source in sources {
1474 for symbol in source.values.into_values() {
1475 if NAMES.contains(&symbol.name.as_str()) {
1476 globals.values.insert(symbol.name.clone(), symbol);
1477 }
1478 }
1479 }
1480 globals
1481}
1482
1483fn render_ts_compiler_globals(out: &mut String, defs: &PackageDeclaration) {
1484 if !defs.types.contains_key("Function") {
1485 out.push_str(
1486 "interface Function {\n /** The parameters before the first optional, defaulted or rest one. */\n readonly length: number;\n}\n\n",
1487 );
1488 }
1489 out.push_str("interface CallableFunction extends Function {}\n\n");
1490 out.push_str("interface NewableFunction extends Function {}\n\n");
1491 out.push_str("interface IArguments {}\n\n");
1492 out.push_str(
1493 "/** Throw `Error(message)` if `condition` is false. Compiler intrinsic — always in scope, not imported. */\n\
1494 declare function assert(condition: boolean, message?: string): void;\n\n",
1495 );
1496 render_ts_checker_plumbing(out, defs);
1497}
1498
1499fn render_ts_checker_plumbing(out: &mut String, defs: &PackageDeclaration) {
1507 out.push_str("declare const Symbol: { readonly iterator: unique symbol };\n\n");
1508 out.push_str(
1509 "interface Array<T> {\n [index: number]: T;\n [Symbol.iterator](): Iterator<T>;\n}\n\n",
1510 );
1511 for (name, sym) in &defs.types {
1512 if let TypeKind::Interface {
1513 generics, methods, ..
1514 } = &sym.kind
1515 && let Some(m) = methods.get("iterator")
1516 {
1517 let _ = writeln!(
1518 out,
1519 "interface {name}{} {{\n [Symbol.iterator](): {};\n}}\n",
1520 ts_interface_generics(name, generics, "declare "),
1521 ts_type(&m.ret)
1522 );
1523 }
1524 }
1525}
1526
1527fn render_declare_module(out: &mut String, defs: &PackageDeclaration) {
1528 let _ = writeln!(out, "declare module \"{}\" {{", defs.package_name);
1529 render_ts_declarations(out, defs, " ", "export ", &|_| None);
1530 let _ = writeln!(out, "}}\n");
1531}
1532
1533fn render_ts_declarations(
1536 out: &mut String,
1537 defs: &PackageDeclaration,
1538 indent: &str,
1539 export_prefix: &str,
1540 class_parent: &dyn Fn(&ClassExtends) -> Option<String>,
1541) {
1542 for (name, sym) in &defs.values {
1543 if is_hidden_prelude_value(&defs.package_name, name) {
1544 continue;
1545 }
1546 push_doc(out, value_doc(&sym.kind), indent);
1547 render_ts_value(out, name, &sym.kind, indent, export_prefix);
1548 }
1549 let pairs_constructors = defs.package_name.starts_with("submilli:");
1552 for (name, sym) in &defs.types {
1553 if is_hidden_prelude_type(&defs.package_name, name) {
1554 continue;
1555 }
1556 if pairs_constructors && name.ends_with("Constructor") {
1557 continue;
1558 }
1559 if pairs_constructors && defs.types.contains_key(&format!("{name}Constructor")) {
1560 render_ts_type_with_ctor(
1561 out,
1562 &defs.types,
1563 name,
1564 indent,
1565 export_prefix,
1566 !defs.values.contains_key(name),
1567 );
1568 } else {
1569 push_doc(out, type_doc(&sym.kind), indent);
1570 let parent = match &sym.kind {
1571 TypeKind::Class {
1572 extends: Some(extends),
1573 ..
1574 } => class_parent(extends),
1575 _ => None,
1576 };
1577 render_ts_type(
1578 out,
1579 name,
1580 &sym.kind,
1581 indent,
1582 export_prefix,
1583 parent.as_deref(),
1584 );
1585 }
1586 }
1587 for (name, ns) in &defs.namespaces {
1588 render_ts_namespace(out, name, ns, indent, export_prefix);
1589 }
1590}
1591
1592fn is_hidden_prelude_value(package_name: &str, name: &str) -> bool {
1593 package_name == crate::mangle::PRELUDE_PACKAGE
1594 && (name.starts_with("string_")
1595 || matches!(
1596 name,
1597 "string_concat" | "string_eq" | "string_length" | "string_cmp"
1598 ))
1599}
1600
1601fn is_hidden_prelude_type(package_name: &str, name: &str) -> bool {
1602 package_name == crate::mangle::PRELUDE_PACKAGE && name.contains('#')
1603}
1604
1605fn render_ts_value(
1606 out: &mut String,
1607 name: &str,
1608 kind: &ValueKind,
1609 indent: &str,
1610 export_prefix: &str,
1611) {
1612 let reserved_export = export_prefix == "export " && is_ts_reserved_word(name);
1613 let declared_name = if reserved_export {
1614 format!("{name}_")
1615 } else {
1616 name.to_string()
1617 };
1618 let decl_export_prefix = if reserved_export { "" } else { export_prefix };
1619 match kind {
1620 ValueKind::Function {
1621 generics,
1622 params,
1623 ret,
1624 type_predicate,
1625 ..
1626 } => {
1627 let ret = ts_return_type(
1628 params,
1629 ret,
1630 type_predicate.as_ref(),
1631 value_doc(kind).as_ref(),
1632 );
1633 let _ = writeln!(
1634 out,
1635 "{indent}{decl_export_prefix}function {declared_name}{}({}): {ret};",
1636 generics_str(generics),
1637 ts_params_str(params, value_doc(kind).as_ref())
1638 );
1639 }
1640 ValueKind::Let { ty, .. } => {
1641 let _ = writeln!(
1642 out,
1643 "{indent}{decl_export_prefix}let {declared_name}: {};",
1644 ts_type(ty)
1645 );
1646 }
1647 ValueKind::Const { ty, .. } => {
1648 let _ = writeln!(
1649 out,
1650 "{indent}{decl_export_prefix}const {declared_name}: {};",
1651 ts_type(ty)
1652 );
1653 }
1654 }
1655 if reserved_export {
1656 let _ = writeln!(out, "{indent}export {{ {declared_name} as {name} }};");
1657 }
1658 out.push('\n');
1659}
1660
1661fn render_ts_type(
1663 out: &mut String,
1664 name: &str,
1665 kind: &TypeKind,
1666 indent: &str,
1667 export_prefix: &str,
1668 parent: Option<&str>,
1669) {
1670 let inner = format!("{indent} ");
1671 match kind {
1672 TypeKind::Interface {
1673 generics,
1674 methods,
1675 properties,
1676 index,
1677 ..
1678 } => {
1679 let _ = writeln!(
1680 out,
1681 "{indent}{export_prefix}interface {name}{} {{",
1682 ts_interface_generics(name, generics, export_prefix)
1683 );
1684 if let Some(index) = index {
1685 let ro = if index.readonly { "readonly " } else { "" };
1686 let _ = writeln!(out, "{inner}{ro}[key: string]: {};", ts_type(&index.value));
1687 }
1688 for (pname, prop) in properties {
1689 push_doc(out, &prop.doc, &inner);
1690 let ro = if prop.readonly { "readonly " } else { "" };
1691 let opt = if prop.optional { "?" } else { "" };
1692 let pname = member_name(pname);
1693 let _ = writeln!(out, "{inner}{ro}{pname}{opt}: {};", ts_type(&prop.ty));
1694 }
1695 for (mname, m) in methods {
1696 push_doc(out, &m.doc, &inner);
1697 let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1698 let optional = if m.optional { "?" } else { "" };
1699 let mname = method_prefix(mname);
1700 let _ = writeln!(
1701 out,
1702 "{inner}{mname}{optional}{}({}): {ret};",
1703 generics_str(&m.generics),
1704 ts_params_str(&m.params, m.doc.as_ref())
1705 );
1706 }
1707 let _ = writeln!(out, "{indent}}}\n");
1708 }
1709 TypeKind::NumberEnum { variants, .. } => {
1710 let _ = writeln!(out, "{indent}{export_prefix}enum {name} {{");
1711 for (v, value) in variants {
1712 let _ = writeln!(out, "{inner}{v} = {value},");
1713 }
1714 let _ = writeln!(out, "{indent}}}\n");
1715 }
1716 TypeKind::StringEnum { variants, .. } => {
1717 let _ = writeln!(out, "{indent}{export_prefix}enum {name} {{");
1718 for (v, value) in variants {
1719 let _ = writeln!(out, "{inner}{v} = \"{}\",", escape_string_literal(value));
1720 }
1721 let _ = writeln!(out, "{indent}}}\n");
1722 }
1723 TypeKind::Alias { generics, ty, .. } => {
1724 let _ = writeln!(
1725 out,
1726 "{indent}{export_prefix}type {name}{} = {};\n",
1727 generics_str(generics),
1728 ts_type(ty)
1729 );
1730 }
1731 TypeKind::Class {
1732 generics,
1733 fields,
1734 methods,
1735 method_visibility,
1736 statics,
1737 static_visibility,
1738 static_fields,
1739 accessors,
1740 constructor,
1741 constructor_visibility,
1742 ..
1743 } => {
1744 let extends = parent.map(|parent| format!(" extends {parent}"));
1749 let _ = writeln!(
1750 out,
1751 "{indent}{export_prefix}class {name}{}{} {{",
1752 generics_str(generics),
1753 extends.unwrap_or_default()
1754 );
1755 for (fname, field) in static_fields {
1756 if field.visibility == crate::Visibility::Private {
1757 continue;
1758 }
1759 push_doc(out, &field.doc, &inner);
1760 let ro = if field.readonly { "readonly " } else { "" };
1761 let fname = member_name(fname);
1762 let _ = writeln!(out, "{inner}static {ro}{fname}: {};", ts_type(&field.ty));
1763 }
1764 for (mname, m) in statics {
1765 if static_visibility.get(mname) == Some(&crate::Visibility::Private) {
1766 continue;
1767 }
1768 push_doc(out, &m.doc, &inner);
1769 let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1770 let mname = member_name(mname);
1771 let _ = writeln!(
1772 out,
1773 "{inner}static {mname}{}({}): {ret};",
1774 generics_str(&m.generics),
1775 ts_params_str(&m.params, m.doc.as_ref())
1776 );
1777 }
1778 for (fname, field) in fields {
1779 if field.visibility == crate::Visibility::Private
1780 || accessors.iter().any(|a| a.name() == fname)
1781 {
1782 continue;
1783 }
1784 push_doc(out, &field.doc, &inner);
1785 let ro = if field.readonly { "readonly " } else { "" };
1786 let opt = if field.optional { "?" } else { "" };
1787 let fname = member_name(fname);
1788 let _ = writeln!(out, "{inner}{ro}{fname}{opt}: {};", ts_type(&field.ty));
1789 }
1790 render_class_accessors(out, fields, accessors, &inner, ts_type);
1791 if *constructor_visibility == crate::Visibility::Private {
1797 let _ = writeln!(out, "{inner}protected constructor();");
1798 } else {
1799 let _ = writeln!(
1800 out,
1801 "{inner}constructor({});",
1802 ts_params_str(constructor, None)
1803 );
1804 }
1805 for (mname, m) in methods {
1806 if method_visibility.get(mname) == Some(&crate::Visibility::Private) {
1807 continue;
1808 }
1809 push_doc(out, &m.doc, &inner);
1810 let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1811 let optional = if m.optional { "?" } else { "" };
1812 let mname = member_name(mname);
1813 let _ = writeln!(
1814 out,
1815 "{inner}{mname}{optional}{}({}): {ret};",
1816 generics_str(&m.generics),
1817 ts_params_str(&m.params, m.doc.as_ref())
1818 );
1819 }
1820 let _ = writeln!(out, "{indent}}}\n");
1821 }
1822 }
1823}
1824
1825fn render_class_accessors(
1830 out: &mut String,
1831 fields: &std::collections::BTreeMap<String, crate::FieldSig>,
1832 accessors: &[crate::AccessorSig],
1833 inner: &str,
1834 fmt_ty: impl Fn(&Type) -> String,
1835) {
1836 use crate::AccessorSig;
1837 let mut by_name: std::collections::BTreeMap<&str, (Option<&Type>, Option<&crate::Param>)> =
1838 std::collections::BTreeMap::new();
1839 for acc in accessors {
1840 let entry = by_name.entry(acc.name()).or_insert((None, None));
1841 match acc {
1842 AccessorSig::Getter { ret_ty, .. } => entry.0 = Some(ret_ty),
1843 AccessorSig::Setter { param, .. } => entry.1 = Some(param),
1844 }
1845 }
1846 for (name, (get, set)) in by_name {
1847 if fields.get(name).map(|f| f.visibility) == Some(crate::Visibility::Private) {
1848 continue;
1849 }
1850 let name = member_name(name);
1851 match (get, set) {
1852 (Some(r), Some(p)) if r == &p.ty => {
1853 let _ = writeln!(out, "{inner}{name}: {};", fmt_ty(r));
1854 }
1855 (Some(r), Some(p)) => {
1856 let _ = writeln!(out, "{inner}get {name}(): {};", fmt_ty(r));
1857 let _ = writeln!(out, "{inner}set {name}({}: {});", p.name, fmt_ty(&p.ty));
1858 }
1859 (Some(r), None) => {
1860 let _ = writeln!(out, "{inner}readonly {name}: {};", fmt_ty(r));
1861 }
1862 (None, Some(p)) => {
1863 let _ = writeln!(out, "{inner}set {name}({}: {});", p.name, fmt_ty(&p.ty));
1864 }
1865 (None, None) => {}
1866 }
1867 }
1868}
1869
1870fn render_ts_type_with_ctor(
1871 out: &mut String,
1872 types: &BTreeMap<String, TypeSymbol>,
1873 name: &str,
1874 indent: &str,
1875 export_prefix: &str,
1876 synthesize_binding: bool,
1877) {
1878 if let Some(sym) = types.get(name) {
1879 push_doc(out, type_doc(&sym.kind), indent);
1880 render_ts_type(out, name, &sym.kind, indent, export_prefix, None);
1881 }
1882 let ctor = format!("{name}Constructor");
1883 if let Some(sym) = types.get(&ctor) {
1884 push_doc(out, type_doc(&sym.kind), indent);
1885 render_ts_type(out, &ctor, &sym.kind, indent, export_prefix, None);
1886 if synthesize_binding {
1887 let _ = writeln!(out, "{indent}{export_prefix}const {name}: {ctor};\n");
1888 }
1889 }
1890}
1891
1892fn render_ts_namespace(
1893 out: &mut String,
1894 name: &str,
1895 ns: &NamespaceSymbol,
1896 indent: &str,
1897 export_prefix: &str,
1898) {
1899 push_doc(out, &ns.doc, indent);
1900 let _ = writeln!(out, "{indent}{export_prefix}namespace {name} {{");
1901 let inner = format!("{indent} ");
1902 for (vname, sym) in &ns.values {
1903 push_doc(out, value_doc(&sym.kind), &inner);
1904 render_ts_value(out, vname, &sym.kind, &inner, "");
1905 }
1906 for (tname, sym) in &ns.types {
1907 push_doc(out, type_doc(&sym.kind), &inner);
1908 render_ts_type(out, tname, &sym.kind, &inner, "", None);
1909 }
1910 for (nname, sub) in &ns.namespaces {
1911 render_ts_namespace(out, nname, sub, &inner, "");
1912 }
1913 let _ = writeln!(out, "{indent}}}\n");
1914}
1915
1916fn ts_params_str(params: &[Param], doc: Option<&DocComment>) -> String {
1917 rendered_params(params, doc, ts_type)
1918}
1919
1920fn ts_return_type(
1921 params: &[Param],
1922 ret: &Type,
1923 predicate: Option<&TypePredicate>,
1924 doc: Option<&DocComment>,
1925) -> String {
1926 let Some(predicate) = predicate else {
1927 return ts_type(ret);
1928 };
1929 let Some(param) = params.get(predicate.parameter_index as usize) else {
1930 return ts_type(ret);
1931 };
1932 let names = parameter_display_names(params, doc);
1933 let Some(name) = names.get(predicate.parameter_index as usize) else {
1934 return ts_type(ret);
1935 };
1936 let asserted = ts_type(&predicate.asserted_type);
1937 match param.ty {
1938 Type::TypeVar(_) | Type::GenericParam { .. } => {
1939 format!("{name} is {} & {asserted}", ts_type(¶m.ty))
1940 }
1941 _ => format!("{name} is {asserted}"),
1942 }
1943}
1944
1945fn ts_type(ty: &Type) -> String {
1946 match ty {
1947 Type::Refined { original, ty } => format!("({} & {})", ts_type(original), ts_type(ty)),
1948 Type::Number => "number".to_string(),
1949 Type::NumberLiteral(n) => n.0.to_string(),
1950 Type::BigInt => "bigint".to_string(),
1951 Type::BigIntLiteral(digits) => format!("{digits}n"),
1952 Type::String => "string".to_string(),
1953 Type::StringLiteral(s) => format!("\"{}\"", escape_string_literal(s)),
1954 Type::Uint8Array => "Uint8Array".to_string(),
1955 Type::Boolean => "boolean".to_string(),
1956 Type::BooleanLiteral(value) => value.to_string(),
1957 Type::Null => "null".to_string(),
1958 Type::Undefined => "undefined".to_string(),
1959 Type::Void => "void".to_string(),
1960 Type::Unknown => "unknown".to_string(),
1961 Type::NonPrimitive => "object".to_string(),
1962 Type::Function {
1963 params,
1964 ret,
1965 predicate,
1966 has_rest,
1967 optional,
1968 } => {
1969 let params = params
1970 .iter()
1971 .enumerate()
1972 .map(|(i, p)| {
1973 let prefix = if *has_rest && i == params.len().saturating_sub(1) {
1974 "..."
1975 } else {
1976 ""
1977 };
1978 let fixed = params.len().saturating_sub(usize::from(*has_rest));
1979 if i < fixed && i >= fixed.saturating_sub(*optional) {
1980 let shown = crate::types::shown_beside_optional_marker(p);
1981 format!("{prefix}arg{i}?: {}", ts_type(&shown))
1982 } else {
1983 format!("{prefix}arg{i}: {}", ts_type(p))
1984 }
1985 })
1986 .collect::<Vec<_>>()
1987 .join(", ");
1988 let ret = predicate.as_deref().map_or_else(
1989 || ts_type(ret),
1990 |p| format!("arg{} is {}", p.parameter_index, ts_type(&p.asserted_type)),
1991 );
1992 format!("({params}) => {ret}")
1993 }
1994 Type::Object { fields, index } => {
1995 let mut members: Vec<String> = fields
1996 .iter()
1997 .map(|(name, field)| {
1998 let opt = if field.optional { "?" } else { "" };
1999 let ro = if field.readonly { "readonly " } else { "" };
2000 let name = member_name(name);
2001 format!("{ro}{name}{opt}: {}", ts_type(&field.ty))
2002 })
2003 .collect();
2004 if let Some(index) = index {
2005 let ro = if index.readonly { "readonly " } else { "" };
2006 members.push(format!("{ro}[key: string]: {}", ts_type(&index.value)));
2007 }
2008 if members.is_empty() {
2009 "{}".into()
2010 } else {
2011 format!("{{ {} }}", members.join("; "))
2012 }
2013 }
2014 Type::Array(elem) => format!("{}[]", ts_type_array_element(elem)),
2015 Type::Tuple(elements) => {
2016 let elements = elements
2017 .iter()
2018 .enumerate()
2019 .map(|(index, ty)| {
2020 if index >= elements.len().saturating_sub(elements.optional) {
2021 format!("({})?", ts_type(ty))
2022 } else {
2023 ts_type(ty)
2024 }
2025 })
2026 .collect::<Vec<_>>()
2027 .join(", ");
2028 format!("[{elements}]")
2029 }
2030 Type::Readonly(inner) if matches!(inner.as_ref(), Type::InterfaceRef { .. }) => {
2031 format!("Readonly{}", ts_type(inner))
2032 }
2033 Type::Readonly(inner) => format!("readonly {}", ts_type(inner)),
2034 Type::Error => "never".to_string(),
2035 Type::Never => "never".to_string(),
2036 Type::TypeVar(name) | Type::GenericParam { name, .. } => name.clone(),
2037 Type::InterfaceRef { name, args, .. }
2038 | Type::ClassRef { name, args, .. }
2039 | Type::Alias { name, args, .. } => ts_named_type(name, args),
2040 Type::NumberEnum { name, .. } | Type::StringEnum { name, .. } => name.clone(),
2041 Type::Union(members) => crate::types::union_display_order(members)
2042 .into_iter()
2043 .map(ts_type_union_member)
2044 .collect::<Vec<_>>()
2045 .join(" | "),
2046 Type::AliasRef { name, args, .. } => ts_named_type(name, args),
2047 }
2048}
2049
2050fn ts_named_type(name: &str, args: &[Type]) -> String {
2051 if args.is_empty() {
2052 return name.to_string();
2053 }
2054 let args = args.iter().map(ts_type).collect::<Vec<_>>().join(", ");
2055 format!("{name}<{args}>")
2056}
2057
2058fn ts_type_array_element(ty: &Type) -> String {
2059 match ty {
2060 Type::Readonly(inner) if matches!(inner.as_ref(), Type::InterfaceRef { .. }) => ts_type(ty),
2061 Type::Function { .. } | Type::Union(_) | Type::Readonly(_) => format!("({})", ts_type(ty)),
2062 _ => ts_type(ty),
2063 }
2064}
2065
2066fn ts_type_union_member(ty: &Type) -> String {
2067 match ty {
2068 Type::Function { .. } => format!("({})", ts_type(ty)),
2069 _ => ts_type(ty),
2070 }
2071}
2072
2073fn is_ts_reserved_word(name: &str) -> bool {
2074 matches!(
2075 name,
2076 "break"
2077 | "case"
2078 | "catch"
2079 | "class"
2080 | "const"
2081 | "continue"
2082 | "debugger"
2083 | "default"
2084 | "delete"
2085 | "do"
2086 | "else"
2087 | "enum"
2088 | "export"
2089 | "extends"
2090 | "false"
2091 | "finally"
2092 | "for"
2093 | "function"
2094 | "if"
2095 | "import"
2096 | "in"
2097 | "instanceof"
2098 | "new"
2099 | "null"
2100 | "return"
2101 | "super"
2102 | "switch"
2103 | "this"
2104 | "throw"
2105 | "true"
2106 | "try"
2107 | "typeof"
2108 | "var"
2109 | "void"
2110 | "while"
2111 | "with"
2112 | "as"
2113 | "async"
2114 | "await"
2115 | "from"
2116 | "get"
2117 | "let"
2118 | "of"
2119 | "set"
2120 | "static"
2121 | "using"
2122 | "yield"
2123 )
2124}
2125
2126fn json_package_declaration() -> PackageDeclaration {
2127 let mut defs = PackageDeclaration::with_package(crate::mangle::PRELUDE_PACKAGE);
2128 let json_prefix = crate::mangle::prelude(JSON_BUILTIN);
2129 let mut json = NamespaceSymbol {
2130 name: JSON_BUILTIN.to_string(),
2131 mangled_prefix: json_prefix.clone(),
2132 declaration_span: Span::at(FileId::JSON),
2133 values: BTreeMap::new(),
2134 types: BTreeMap::new(),
2135 namespaces: BTreeMap::new(),
2136 doc: None,
2137 };
2138 json.values.insert(
2139 "stringify".to_string(),
2140 ValueSymbol {
2141 name: "stringify".to_string(),
2142 mangled_name: crate::mangle::extend(&json_prefix, "stringify"),
2143 declaration_span: Span::at(FileId::JSON),
2144 kind: ValueKind::Function {
2145 generics: vec!["T".to_string()],
2146 params: vec![
2147 Param::new("value", Type::TypeVar("T".to_string())),
2148 Param {
2149 name: "replacer".to_string(),
2150 ty: Type::union(vec![Type::Null, Type::Undefined]),
2151 default: Some(crate::DefaultValue::Undefined),
2152 optional: false,
2153 rest: false,
2154 },
2155 Param {
2156 name: "space".to_string(),
2157 ty: Type::union(vec![Type::Number, Type::String, Type::Null, Type::Undefined]),
2158 default: Some(crate::DefaultValue::Undefined),
2159 optional: false,
2160 rest: false,
2161 },
2162 ],
2163 ret: Type::union(vec![Type::String, Type::Undefined]),
2164 type_predicate: None,
2165 doc: crate::doc(FileId::JSON, "/** Serialize a value to a JSON string, or return undefined when it has no JSON representation. */"),
2166 },
2167 },
2168 );
2169 json.values.insert(
2170 "parse".to_string(),
2171 ValueSymbol {
2172 name: "parse".to_string(),
2173 mangled_name: crate::mangle::extend(&json_prefix, "parse"),
2174 declaration_span: Span::at(FileId::JSON),
2175 kind: ValueKind::Function {
2176 generics: Vec::new(),
2177 params: vec![Param::new("text", Type::String)],
2178 ret: Type::Unknown,
2179 type_predicate: None,
2180 doc: crate::doc(
2181 FileId::JSON,
2182 "/** Parse a JSON string as unknown; use `JSON.parse(s) as T` to validate a target type. */",
2183 ),
2184 },
2185 },
2186 );
2187 defs.namespaces.insert(JSON_BUILTIN.to_string(), json);
2188 defs
2189}
2190
2191fn render_value(out: &mut String, name: &str, kind: &ValueKind, indent: &str) {
2192 match kind {
2193 ValueKind::Function {
2194 generics,
2195 params,
2196 ret,
2197 ..
2198 } => {
2199 let _ = writeln!(
2200 out,
2201 "{indent}function {name}{}({}): {ret};\n",
2202 generics_str(generics),
2203 params_str(params, value_doc(kind).as_ref())
2204 );
2205 }
2206 ValueKind::Let { ty, .. } => {
2207 let _ = writeln!(out, "{indent}let {name}: {ty};\n");
2208 }
2209 ValueKind::Const { ty, .. } => {
2210 let _ = writeln!(out, "{indent}const {name}: {ty};\n");
2211 }
2212 }
2213}
2214
2215fn render_type(out: &mut String, name: &str, kind: &TypeKind, indent: &str) {
2216 let inner = format!("{indent} ");
2217 match kind {
2218 TypeKind::Interface {
2219 generics,
2220 methods,
2221 properties,
2222 ..
2223 } => {
2224 let _ = writeln!(out, "{indent}interface {name}{} {{", generics_str(generics));
2225 for (pname, prop) in properties {
2226 push_doc(out, &prop.doc, &inner);
2227 let ro = if prop.readonly { "readonly " } else { "" };
2228 let opt = if prop.optional { "?" } else { "" };
2229 let pname = member_name(pname);
2230 let _ = writeln!(out, "{inner}{ro}{pname}{opt}: {};", prop.ty);
2231 }
2232 for (mname, m) in methods {
2233 push_doc(out, &m.doc, &inner);
2234 let mname = method_prefix(mname);
2235 let _ = writeln!(
2236 out,
2237 "{inner}{mname}{}{}({}): {};",
2238 if m.optional { "?" } else { "" },
2239 generics_str(&m.generics),
2240 params_str(&m.params, m.doc.as_ref()),
2241 m.ret
2242 );
2243 }
2244 let _ = writeln!(out, "{indent}}}\n");
2245 }
2246 TypeKind::NumberEnum { variants, .. } => {
2247 let _ = writeln!(out, "{indent}enum {name} {{");
2248 for (v, value) in variants {
2249 let _ = writeln!(out, "{inner}{v} = {value},");
2250 }
2251 let _ = writeln!(out, "{indent}}}\n");
2252 }
2253 TypeKind::StringEnum { variants, .. } => {
2254 let _ = writeln!(out, "{indent}enum {name} {{");
2255 for (v, value) in variants {
2256 let _ = writeln!(out, "{inner}{v} = \"{}\",", escape_string_literal(value));
2257 }
2258 let _ = writeln!(out, "{indent}}}\n");
2259 }
2260 TypeKind::Alias { generics, ty, .. } => {
2261 let _ = writeln!(
2262 out,
2263 "{indent}type {name}{} = {ty};\n",
2264 generics_str(generics)
2265 );
2266 }
2267 TypeKind::Class {
2268 generics,
2269 fields,
2270 methods,
2271 method_visibility,
2272 statics,
2273 static_visibility,
2274 static_fields,
2275 accessors,
2276 constructor,
2277 constructor_visibility,
2278 ..
2279 } => {
2280 let _ = writeln!(out, "{indent}class {name}{} {{", generics_str(generics));
2285 for (fname, field) in static_fields {
2286 if field.visibility == crate::Visibility::Private {
2287 continue;
2288 }
2289 push_doc(out, &field.doc, &inner);
2290 let ro = if field.readonly { "readonly " } else { "" };
2291 let fname = member_name(fname);
2292 let _ = writeln!(out, "{inner}static {ro}{fname}: {};", field.ty);
2293 }
2294 for (mname, m) in statics {
2295 if static_visibility.get(mname) == Some(&crate::Visibility::Private) {
2296 continue;
2297 }
2298 push_doc(out, &m.doc, &inner);
2299 let mname = member_name(mname);
2300 let _ = writeln!(
2301 out,
2302 "{inner}static {mname}{}({}): {};",
2303 generics_str(&m.generics),
2304 params_str(&m.params, m.doc.as_ref()),
2305 m.ret
2306 );
2307 }
2308 for (fname, field) in fields {
2309 if field.visibility == crate::Visibility::Private
2310 || accessors.iter().any(|a| a.name() == fname)
2311 {
2312 continue;
2313 }
2314 push_doc(out, &field.doc, &inner);
2315 let ro = if field.readonly { "readonly " } else { "" };
2316 let opt = if field.optional { "?" } else { "" };
2317 let fname = member_name(fname);
2318 let _ = writeln!(out, "{inner}{ro}{fname}{opt}: {};", field.ty);
2319 }
2320 render_class_accessors(out, fields, accessors, &inner, ToString::to_string);
2321 if *constructor_visibility != crate::Visibility::Private {
2322 let _ = writeln!(
2323 out,
2324 "{inner}constructor({});",
2325 params_str(constructor, None)
2326 );
2327 }
2328 for (mname, m) in methods {
2329 if method_visibility.get(mname) == Some(&crate::Visibility::Private) {
2330 continue;
2331 }
2332 push_doc(out, &m.doc, &inner);
2333 let mname = member_name(mname);
2334 let _ = writeln!(
2335 out,
2336 "{inner}{mname}{}{}({}): {};",
2337 if m.optional { "?" } else { "" },
2338 generics_str(&m.generics),
2339 params_str(&m.params, m.doc.as_ref()),
2340 m.ret
2341 );
2342 }
2343 let _ = writeln!(out, "{indent}}}\n");
2344 }
2345 }
2346}
2347
2348fn render_type_with_ctor(
2353 out: &mut String,
2354 types: &BTreeMap<String, TypeSymbol>,
2355 name: &str,
2356 indent: &str,
2357) {
2358 if let Some(sym) = types.get(name) {
2359 push_doc(out, type_doc(&sym.kind), indent);
2360 render_type(out, name, &sym.kind, indent);
2361 }
2362 let ctor = format!("{name}Constructor");
2363 if let Some(sym) = types.get(&ctor) {
2364 push_doc(out, type_doc(&sym.kind), indent);
2365 render_type(out, &ctor, &sym.kind, indent);
2366 let _ = writeln!(out, "{indent}const {name}: {ctor};\n");
2367 }
2368}
2369
2370fn render_namespace(out: &mut String, name: &str, ns: &NamespaceSymbol, indent: &str) {
2371 let _ = writeln!(out, "{indent}namespace {name} {{");
2372 let inner = format!("{indent} ");
2373 for (vname, sym) in &ns.values {
2374 push_doc(out, value_doc(&sym.kind), &inner);
2375 render_value(out, vname, &sym.kind, &inner);
2376 }
2377 for (tname, sym) in &ns.types {
2378 push_doc(out, type_doc(&sym.kind), &inner);
2381 render_type(out, tname, &sym.kind, &inner);
2382 }
2383 for (nname, sub) in &ns.namespaces {
2384 render_namespace(out, nname, sub, &inner);
2385 }
2386 let _ = writeln!(out, "{indent}}}\n");
2387}
2388
2389fn generics_str(generics: &[String]) -> String {
2390 if generics.is_empty() {
2391 String::new()
2392 } else {
2393 format!("<{}>", generics.join(", "))
2394 }
2395}
2396
2397fn ts_interface_generics(name: &str, generics: &[String], export_prefix: &str) -> String {
2401 if export_prefix == "declare "
2402 && matches!(name, "Iterable" | "Iterator")
2403 && let [t] = generics
2404 {
2405 return format!("<{t}, TReturn = any, TNext = any>");
2406 }
2407 generics_str(generics)
2408}
2409
2410fn params_str(params: &[Param], doc: Option<&DocComment>) -> String {
2411 rendered_params(params, doc, ToString::to_string)
2412}
2413
2414fn rendered_params(
2415 params: &[Param],
2416 doc: Option<&DocComment>,
2417 format_type: impl Fn(&Type) -> String,
2418) -> String {
2419 let names = parameter_display_names(params, doc);
2420 params
2421 .iter()
2422 .zip(names)
2423 .enumerate()
2424 .map(|(index, (p, name))| {
2425 let prefix = if p.rest { "..." } else { "" };
2426 let later_required = params
2428 .iter()
2429 .skip(index.saturating_add(1))
2430 .any(|later| !later.is_omittable() && !later.rest);
2431 let opt = if p.is_omittable() && !later_required {
2432 "?"
2433 } else {
2434 ""
2435 };
2436 let ty = if opt.is_empty() {
2438 p.ty.clone()
2439 } else {
2440 crate::types::shown_beside_optional_marker(&p.ty)
2441 };
2442 format!("{prefix}{name}{opt}: {}", format_type(&ty))
2443 })
2444 .collect::<Vec<_>>()
2445 .join(", ")
2446}
2447
2448fn parameter_display_names(params: &[Param], doc: Option<&DocComment>) -> Vec<String> {
2451 let tags: Vec<_> = doc
2452 .into_iter()
2453 .flat_map(|doc| &doc.params)
2454 .filter(|tag| !tag.name.contains('.'))
2455 .collect();
2456 let mut used: BTreeSet<String> = params
2457 .iter()
2458 .filter(|p| !crate::lower_patterns::is_pattern_param(&p.name))
2459 .map(|p| p.name.clone())
2460 .collect();
2461 params
2462 .iter()
2463 .enumerate()
2464 .map(|(index, param)| {
2465 if !crate::lower_patterns::is_pattern_param(¶m.name) {
2466 return param.name.clone();
2467 }
2468 if let Some(tag) = tags.get(index)
2469 && valid_parameter_name(&tag.name)
2470 && used.insert(tag.name.clone())
2471 {
2472 return tag.name.clone();
2473 }
2474 let mut name = format!("arg{index}");
2475 while !used.insert(name.clone()) {
2476 name.push('_');
2477 }
2478 name
2479 })
2480 .collect()
2481}
2482
2483fn valid_parameter_name(name: &str) -> bool {
2484 let mut chars = name.chars();
2485 chars
2486 .next()
2487 .is_some_and(|c| c.is_ascii_alphabetic() || matches!(c, '_' | '$'))
2488 && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '$'))
2489 && !is_ts_reserved_word(name)
2490 && !matches!(
2491 name,
2492 "arguments"
2493 | "eval"
2494 | "implements"
2495 | "interface"
2496 | "package"
2497 | "private"
2498 | "protected"
2499 | "public"
2500 )
2501}
2502
2503fn push_doc(out: &mut String, doc: &Option<DocComment>, indent: &str) {
2504 let Some(doc) = doc else { return };
2505 let mut lines: Vec<String> = doc.summary.lines().map(str::to_string).collect();
2506 for p in &doc.params {
2507 lines.push(format!("@param {} {}", p.name, p.description));
2508 }
2509 for cap in &doc.capabilities {
2510 let mut line = format!("@capability {}", cap.capability);
2511 if !cap.bindings.is_empty() {
2512 let bindings = cap
2513 .bindings
2514 .iter()
2515 .map(|b| {
2516 let value = match &b.kind {
2517 DocCapabilityBindingKind::Parameter { param, path, .. }
2518 if *param == b.field && path.is_empty() =>
2519 {
2520 String::new()
2521 }
2522 DocCapabilityBindingKind::Parameter { param, path, .. } => {
2523 let suffix = path
2524 .iter()
2525 .map(|part| format!(".{part}"))
2526 .collect::<String>();
2527 format!(": ${param}{suffix}")
2528 }
2529 DocCapabilityBindingKind::Type { name, .. } => format!(": {name}"),
2530 DocCapabilityBindingKind::Literal { value, .. } => {
2531 format!(": {}", literal_str(value))
2532 }
2533 };
2534 format!("{}{}", b.field, value)
2535 })
2536 .collect::<Vec<_>>()
2537 .join(", ");
2538 line.push_str(&format!(" {{ {bindings} }}"));
2539 }
2540 if !cap.description.is_empty() {
2541 line.push_str(" - ");
2542 line.push_str(&cap.description);
2543 }
2544 lines.push(line);
2545 }
2546 if let Some(r) = &doc.returns {
2547 lines.push(format!("@returns {}", r.description));
2548 }
2549 if lines.is_empty() {
2550 return;
2551 }
2552 let _ = writeln!(out, "{indent}/**");
2553 for line in lines {
2554 let _ = writeln!(out, "{indent} * {line}");
2555 }
2556 let _ = writeln!(out, "{indent} */");
2557}
2558
2559fn literal_str(value: &DocCapabilityLiteral) -> String {
2560 match value {
2561 DocCapabilityLiteral::String(s) => {
2562 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
2563 }
2564 DocCapabilityLiteral::Number(n) => n.clone(),
2565 DocCapabilityLiteral::Boolean(v) => v.to_string(),
2566 DocCapabilityLiteral::Null => "null".to_string(),
2567 }
2568}
2569
2570fn value_doc(kind: &ValueKind) -> &Option<DocComment> {
2571 match kind {
2572 ValueKind::Function { doc, .. }
2573 | ValueKind::Let { doc, .. }
2574 | ValueKind::Const { doc, .. } => doc,
2575 }
2576}
2577
2578fn type_doc(kind: &TypeKind) -> &Option<DocComment> {
2579 match kind {
2580 TypeKind::Interface { doc, .. }
2581 | TypeKind::Class { doc, .. }
2582 | TypeKind::NumberEnum { doc, .. }
2583 | TypeKind::StringEnum { doc, .. }
2584 | TypeKind::Alias { doc, .. } => doc,
2585 }
2586}
2587
2588fn method_prefix(name: &str) -> Cow<'_, str> {
2592 match name {
2593 "@call" => Cow::Borrowed(""),
2594 "new" => Cow::Borrowed("new "),
2595 _ => member_name(name),
2596 }
2597}
2598
2599fn member_name(name: &str) -> Cow<'_, str> {
2604 if name.is_ascii() && crate::type_rendering::is_identifier_name(name) {
2605 return Cow::Borrowed(name);
2606 }
2607 Cow::Owned(format!("\"{}\"", escape_string_literal(name)))
2608}
2609
2610#[cfg(test)]
2611mod tests {
2612 use super::*;
2613
2614 #[test]
2615 fn member_names_are_quoted_unless_identifiers() {
2616 let mut lone = String::new();
2617 crate::literal_units::push_lone_surrogate(&mut lone, 0xD800);
2618 assert_eq!(member_name("count"), "count");
2619 assert_eq!(member_name("$el_2"), "$el_2");
2620 assert_eq!(member_name("café"), "\"café\"");
2621 assert_eq!(member_name("a b"), "\"a b\"");
2622 assert_eq!(member_name("0"), "\"0\"");
2623 assert_eq!(member_name(&lone), "\"\\uD800\"");
2624 }
2625
2626 #[test]
2627 fn destructured_display_names_are_valid_and_distinct() {
2628 let params = [
2629 Param::new("#pattern_p_0", Type::Number),
2630 Param::new("arg0", Type::Number),
2631 Param::new("", Type::Number),
2632 Param::new("#pattern_p_3", Type::Number),
2633 ];
2634 let doc = crate::doc(
2635 FileId(0),
2636 "/** Names.\n * @param class Reserved.\n * @param arg0 Existing.\n * @param arg0.x Property.\n * @param pair Pair.\n * @param pair Duplicate.\n */",
2637 );
2638 assert_eq!(
2639 parameter_display_names(¶ms, doc.as_ref()),
2640 ["arg0_", "arg0", "pair", "arg3"]
2641 );
2642 assert_eq!(
2643 params_str(¶ms, doc.as_ref()),
2644 "arg0_: number, arg0: number, pair: number, arg3: number"
2645 );
2646 assert_eq!(
2647 ts_params_str(¶ms, doc.as_ref()),
2648 "arg0_: number, arg0: number, pair: number, arg3: number"
2649 );
2650 }
2651
2652 #[test]
2653 fn catalog_filters_before_counting_omitted_entries() {
2654 let extra = (0..CATALOG_LIMIT)
2655 .map(|index| CatalogEntry {
2656 name: format!("@pied-piper/package{index}"),
2657 source: "registry".into(),
2658 description: String::new(),
2659 })
2660 .collect();
2661 let catalog = catalog_filtered(extra, |name| !name.starts_with("submilli:"));
2662 assert_eq!(catalog.entries.len(), CATALOG_LIMIT);
2663 assert_eq!(catalog.remaining, 0);
2664 assert!(
2665 catalog
2666 .entries
2667 .iter()
2668 .all(|entry| entry.name.starts_with("@pied-piper/"))
2669 );
2670 }
2671
2672 #[test]
2673 fn http_docs_render_signatures() {
2674 let doc = docs("submilli:http").expect("http module");
2675 assert_eq!(doc.name, "submilli:http");
2676 assert!(!doc.description.is_empty());
2677 assert!(doc.declarations.contains("function get("));
2678 assert!(doc.declarations.contains("function post("));
2679 assert!(doc.declarations.contains("@capability http.get"));
2680 assert!(doc.declarations.contains("@capability http.download"));
2681 }
2682
2683 #[test]
2684 fn fs_and_package_configuration_docs_render_capabilities() {
2685 let fs = docs("submilli:fs").expect("fs module");
2686 assert!(fs.declarations.contains("@capability fs.read"));
2687 assert!(fs.declarations.contains("@capability fs.write"));
2688 assert!(fs.declarations.contains("@capability fs.list"));
2689
2690 let secrets = docs("submilli:secrets").expect("secrets module");
2691 assert!(secrets.declarations.contains("@capability secrets.get"));
2692 let variables = docs("submilli:variables").expect("variables module");
2693 assert!(variables.declarations.contains("@capability variables.get"));
2694 assert!(
2695 variables
2696 .declarations
2697 .contains("get(name: string): string | undefined")
2698 );
2699 }
2700
2701 #[test]
2702 fn security_is_not_user_facing() {
2703 assert!(docs("submilli:security").is_none());
2704 assert!(!search("").iter().any(|m| m.name == INTERNAL_MODULE));
2705 }
2706
2707 #[test]
2708 fn search_matches_symbol_names() {
2709 let hits = search("sha256");
2710 assert!(
2711 hits.iter().any(|m| m.name == "submilli:crypto"),
2712 "{:?}",
2713 names(&hits)
2714 );
2715 }
2716
2717 #[test]
2718 fn empty_query_lists_all_user_modules() {
2719 let names = names(&search(""));
2720 assert!(names.contains(&"submilli:crypto".to_string()));
2721 assert!(names.contains(&"submilli:http".to_string()));
2722 assert!(!names.contains(&INTERNAL_MODULE.to_string()));
2723 }
2724
2725 fn names(hits: &[ModuleSummary]) -> Vec<String> {
2726 hits.iter().map(|m| m.name.clone()).collect()
2727 }
2728
2729 #[test]
2730 fn builtins_lists_headline_types_and_namespaces() {
2731 let b = builtins();
2732 for expected in ["Array", "Map", "Set", "String", "Number", "Error"] {
2733 assert!(
2734 b.types.contains(&expected.to_string()),
2735 "missing {expected}"
2736 );
2737 }
2738 for hidden in [
2740 "ArrayConstructor",
2741 "Console",
2742 "Iterator",
2743 "Temporal#Instant",
2744 ] {
2745 assert!(!b.types.contains(&hidden.to_string()), "leaked {hidden}");
2746 }
2747 for ns in ["Math", "Temporal", "JSON"] {
2748 assert!(
2749 b.namespaces.contains(&ns.to_string()),
2750 "missing namespace {ns}"
2751 );
2752 }
2753 }
2754
2755 #[test]
2756 fn every_builtin_has_renderable_docs() {
2757 let b = builtins();
2758 for name in b.types.iter().chain(b.namespaces.iter()) {
2759 assert!(
2760 builtin_docs(name).is_some_and(|d| !d.is_empty()),
2761 "no docs for built-in {name}"
2762 );
2763 }
2764 }
2765
2766 #[test]
2767 fn dotted_path_returns_the_member_slice() {
2768 let slice = builtin_docs("Temporal.Instant").expect("Temporal.Instant");
2769 assert!(slice.contains("const Instant: Temporal.InstantConstructor;"));
2772 assert!(slice.contains("interface Instant {"));
2773 assert!(slice.contains("interface InstantConstructor {"));
2774 assert!(slice.starts_with("namespace Temporal {"), "{slice}");
2776 assert!(slice.contains("A point in time, to nanosecond precision"));
2778 assert!(!slice.contains("interface ZonedDateTime {"), "{slice}");
2780
2781 let full = builtin_docs("Temporal").expect("Temporal");
2782 assert!(
2783 slice.len() * 4 < full.len(),
2784 "slice {} vs full {}",
2785 slice.len(),
2786 full.len()
2787 );
2788 }
2789
2790 #[test]
2791 fn dotted_path_resolves_nested_namespaces_and_values() {
2792 let now = builtin_docs("Temporal.Now").expect("Temporal.Now");
2793 assert!(now.starts_with("namespace Temporal {"));
2794 assert!(now.contains("namespace Now {"));
2795 assert!(now.contains("function instant(): Temporal.Instant;"));
2796
2797 let one = builtin_docs("Temporal.Now.instant").expect("Temporal.Now.instant");
2798 assert!(one.contains("function instant(): Temporal.Instant;"));
2799 assert!(!one.contains("plainDateISO"), "{one}");
2800
2801 let max = builtin_docs("Math.max").expect("Math.max");
2802 assert!(max.contains("function max("), "{max}");
2803 assert!(!max.contains("function min("), "{max}");
2804
2805 let parse = builtin_docs("JSON.parse").expect("JSON.parse");
2806 assert!(parse.contains("function parse("), "{parse}");
2807 assert!(!parse.contains("function stringify("), "{parse}");
2808 }
2809
2810 #[test]
2811 fn dotted_path_resolves_members_of_a_type_head() {
2812 let is_array = builtin_docs("Array.isArray").expect("Array.isArray");
2815 assert!(
2816 is_array.starts_with("interface ArrayConstructor"),
2817 "{is_array}"
2818 );
2819 assert!(is_array.contains("isArray"));
2820
2821 let repeat = builtin_docs("String.repeat").expect("String.repeat");
2822 assert!(repeat.starts_with("interface String"), "{repeat}");
2823 assert!(
2824 repeat.contains("repeat(count: number): string;"),
2825 "{repeat}"
2826 );
2827
2828 let from = builtin_docs("Temporal.Instant.from").expect("Temporal.Instant.from");
2831 assert!(from.starts_with("namespace Temporal {"), "{from}");
2832 assert!(from.contains("interface InstantConstructor {"), "{from}");
2833 assert!(!from.contains("namespace Instant"), "{from}");
2834 }
2835
2836 #[test]
2837 fn every_member_a_miss_names_is_itself_resolvable() {
2838 let b = builtins();
2842 for name in b.types.iter().chain(b.namespaces.iter()) {
2843 let BuiltinLookup::UnknownMember { members, .. } =
2844 builtin_lookup(&format!("{name}.__definitely_not_a_member"))
2845 else {
2846 continue;
2847 };
2848 for member in members {
2849 let path = format!("{name}.{member}");
2850 assert!(
2851 matches!(builtin_lookup(&path), BuiltinLookup::Found(_)),
2852 "`{path}` is offered as a member of `{name}` but does not resolve"
2853 );
2854 }
2855 }
2856 }
2857
2858 #[test]
2859 fn unknown_member_lists_the_members_that_exist() {
2860 let BuiltinLookup::UnknownMember {
2861 path,
2862 member,
2863 members,
2864 } = builtin_lookup("Temporal.Foo")
2865 else {
2866 panic!("expected an unknown-member miss");
2867 };
2868 assert_eq!(path, "Temporal");
2869 assert_eq!(member, "Foo");
2870 assert!(members.contains(&"Instant".to_string()));
2871 assert!(members.contains(&"Now".to_string()));
2872 assert!(
2874 !members.contains(&"InstantConstructor".to_string()),
2875 "{members:?}"
2876 );
2877
2878 let BuiltinLookup::UnknownMember { path, members, .. } = builtin_lookup("Math.max.foo")
2881 else {
2882 panic!("expected an unknown-member miss");
2883 };
2884 assert_eq!(path, "Math.max");
2885 assert!(members.is_empty());
2886 }
2887
2888 #[test]
2889 fn a_bad_member_is_reported_against_its_full_path() {
2890 for (query, expected_path) in [
2891 ("Temporal.Instant.nope", "Temporal.Instant"),
2892 ("Temporal.Now.nope", "Temporal.Now"),
2893 ("Array.nope", "Array"),
2894 ("Temporal.Instant.from.x", "Temporal.Instant.from"),
2895 ("Array.isArray.foo", "Array.isArray"),
2898 ] {
2899 let BuiltinLookup::UnknownMember { path, member, .. } = builtin_lookup(query) else {
2900 panic!("expected an unknown-member miss for {query}");
2901 };
2902 assert_eq!(path, expected_path, "for {query}");
2903 assert!(!member.is_empty(), "for {query}");
2904 }
2905 }
2906
2907 #[test]
2908 fn unknown_head_is_not_an_unknown_member() {
2909 assert_eq!(builtin_lookup("Bogus.Thing"), BuiltinLookup::Unknown);
2910 assert_eq!(builtin_lookup("Temporal#Instant"), BuiltinLookup::Unknown);
2912 assert_eq!(builtin_lookup(""), BuiltinLookup::Unknown);
2913 }
2914
2915 #[test]
2916 fn plain_names_are_unchanged_and_segments_are_case_insensitive() {
2917 let full = builtin_docs("Temporal").expect("Temporal");
2919 assert!(full.starts_with("namespace Temporal {"));
2920 for member in ["Instant", "ZonedDateTime", "PlainDate", "Duration"] {
2921 assert!(
2922 full.contains(&format!("interface {member} {{")),
2923 "missing {member}"
2924 );
2925 }
2926 assert_eq!(
2927 builtin_docs("temporal.instant"),
2928 builtin_docs("Temporal.Instant")
2929 );
2930 assert_eq!(builtin_docs("Temporal."), builtin_docs("Temporal"));
2932 }
2933
2934 #[test]
2935 fn resolve_prefers_a_module_then_falls_back_to_builtins() {
2936 assert!(matches!(resolve("submilli:http"), Resolution::Module(_)));
2937 assert!(matches!(resolve("Temporal"), Resolution::Builtin { .. }));
2938 assert!(matches!(
2939 resolve("Temporal.Instant"),
2940 Resolution::Builtin { .. }
2941 ));
2942 assert!(matches!(
2943 resolve("Temporal.Foo"),
2944 Resolution::UnknownMember { .. }
2945 ));
2946 assert!(matches!(resolve("nope"), Resolution::Unknown));
2947 assert!(matches!(resolve("submilli:security"), Resolution::Unknown));
2948 }
2949
2950 #[test]
2951 fn suggest_reaches_past_bare_edit_distance() {
2952 let extra = vec!["@mcp/linear".to_string()];
2953 assert_eq!(suggest("http", &extra).as_deref(), Some("submilli:http"));
2956 assert_eq!(suggest("htp", &extra).as_deref(), Some("submilli:http"));
2957 assert_eq!(suggest("linear", &extra).as_deref(), Some("@mcp/linear"));
2958 assert_eq!(suggest("Temporel", &extra).as_deref(), Some("Temporal"));
2960 assert_eq!(
2961 suggest("Temporel.Instant", &extra).as_deref(),
2962 Some("Temporal")
2963 );
2964 assert_eq!(suggest("Aray", &extra).as_deref(), Some("Array"));
2965 assert_eq!(suggest("zzzzzzzzz", &extra), None);
2966 assert_ne!(
2969 suggest(INTERNAL_MODULE, &[]).as_deref(),
2970 Some(INTERNAL_MODULE)
2971 );
2972 }
2973
2974 #[test]
2975 fn a_deliberately_omitted_global_points_at_its_replacement() {
2976 assert_eq!(suggest("Date", &[]).as_deref(), Some("Temporal"));
2979 assert_eq!(suggest("date", &[]).as_deref(), Some("Temporal"));
2980 }
2981
2982 #[test]
2983 fn suggest_declines_degenerate_and_self_queries() {
2984 for q in ["", ".", "a", "xy"] {
2987 assert_eq!(suggest(q, &[]), None, "for {q:?}");
2988 }
2989 assert_eq!(suggest(".Temporel", &[]).as_deref(), Some("Temporal"));
2991 let declared = vec!["@pied-piper/mypkg".to_string()];
2993 assert_eq!(suggest("@pied-piper/mypkg", &declared), None);
2994 }
2995
2996 #[test]
2997 fn catalog_is_summary_only_and_bounded() {
2998 let stdlib = catalog(Vec::new());
2999 assert_eq!(stdlib.remaining, 0);
3000 assert!(stdlib.entries.iter().all(|e| e.source == SOURCE_STDLIB));
3001 assert!(stdlib.entries.iter().any(|e| e.name == "submilli:http"));
3002 assert!(stdlib.entries.iter().all(|e| !e.description.is_empty()));
3003 assert!(!stdlib.entries.iter().any(|e| e.name == INTERNAL_MODULE));
3004
3005 let extra: Vec<CatalogEntry> = (0..CATALOG_LIMIT)
3006 .map(|i| CatalogEntry {
3007 name: format!("@pied-piper/p{i}"),
3008 source: "registry".to_string(),
3009 description: "x".to_string(),
3010 })
3011 .collect();
3012 let big = catalog(extra);
3013 assert_eq!(big.entries.len(), CATALOG_LIMIT);
3014 assert_eq!(big.remaining, stdlib.entries.len());
3015 }
3016
3017 #[test]
3018 fn miss_messages_name_the_repair() {
3019 let msg = unknown_member_message("Temporal", "Foo", &["Instant".into(), "Now".into()]);
3020 assert!(msg.contains("Temporal"), "{msg}");
3021 assert!(msg.contains("Instant, Now"), "{msg}");
3022 let leaf = unknown_member_message("Math.max", "foo", &[]);
3023 assert!(leaf.contains("no members"), "{leaf}");
3024 assert!(builtin_no_import_note("Temporal").contains("import"));
3026 }
3027
3028 fn every_package() -> Stdlib {
3031 crate::stdlib::OptionalPackage::ALL
3032 .iter()
3033 .fold(Stdlib::core(), |set, package| set.with(*package))
3034 }
3035
3036 #[test]
3037 fn stdlib_capability_tags_parse_cleanly() {
3038 for module in user_modules(every_package()) {
3039 for symbol in module.values.values() {
3040 let Some(doc) = value_doc(&symbol.kind) else {
3041 continue;
3042 };
3043 for cap in &doc.capabilities {
3044 assert!(
3045 cap.diagnostics.is_empty(),
3046 "{}.{}: {:?}",
3047 module.package_name,
3048 symbol.name,
3049 cap.diagnostics
3050 );
3051 }
3052 }
3053 }
3054 }
3055
3056 #[test]
3057 fn every_importable_stdlib_symbol_has_docs() {
3058 for module in user_modules(every_package()) {
3059 for symbol in module.values.values() {
3060 assert!(
3061 value_doc(&symbol.kind).is_some(),
3062 "{}.{} is missing docs",
3063 module.package_name,
3064 symbol.name
3065 );
3066 }
3067 for symbol in module.types.values() {
3068 assert!(
3069 type_doc(&symbol.kind).is_some(),
3070 "{}.{} is missing docs",
3071 module.package_name,
3072 symbol.name
3073 );
3074 }
3075 }
3076 }
3077
3078 #[test]
3079 fn builtin_docs_print_a_call_signature_without_a_name() {
3080 for name in ["Number", "Number.@call"] {
3081 let docs = builtin_docs(name).expect("Number built-in");
3082 assert!(docs.contains(" (value: "), "{docs}");
3083 assert!(!docs.contains("@call"), "{docs}");
3084 }
3085 }
3086
3087 #[test]
3088 fn builtin_docs_folds_constructor_into_array() {
3089 let docs = builtin_docs("Array").expect("Array built-in");
3090 assert!(docs.contains("interface Array<"));
3091 assert!(docs.contains("interface ArrayConstructor"));
3092 assert!(docs.contains("const Array: ArrayConstructor;"));
3093 }
3094
3095 #[test]
3096 fn builtin_docs_renders_temporal_namespace() {
3097 let docs = builtin_docs("Temporal").expect("Temporal built-in");
3098 assert!(docs.starts_with("namespace Temporal {"));
3099 assert!(docs.contains("interface Instant"));
3100 assert!(docs.contains("namespace Now {"));
3101 assert!(docs.contains(" interface Instant"));
3103 assert_eq!(docs.matches("const Instant").count(), 1, "{docs}");
3106 }
3107
3108 #[test]
3109 fn builtin_docs_handles_json_intrinsic() {
3110 let docs = builtin_docs("JSON").expect("JSON built-in");
3111 assert!(docs.contains(
3112 "function stringify<T>(value: T, replacer?: null, space?: number | string | null): string | undefined;"
3113 ));
3114 assert!(docs.contains("function parse(text: string): unknown;"));
3115 }
3116
3117 #[test]
3118 fn builtin_docs_is_case_insensitive() {
3119 assert_eq!(builtin_docs("array"), builtin_docs("Array"));
3121 assert_eq!(builtin_docs("temporal"), builtin_docs("Temporal"));
3122 assert_eq!(builtin_docs("json"), builtin_docs("JSON"));
3123 assert_eq!(builtin_docs("ARRAY"), builtin_docs("Array"));
3125 }
3126
3127 #[test]
3128 fn builtin_docs_unknown_is_none() {
3129 assert!(builtin_docs("Promise").is_none());
3130 assert!(builtin_docs("Temporal#Instant").is_none());
3131 }
3132
3133 #[test]
3134 fn packages_d_ts_imports_the_types_a_package_borrows() {
3135 let mut package = PackageDeclaration::with_package("@pied-piper/files");
3136 let download = crate::stdlib::http::package_declaration()
3137 .values
3138 .get("download")
3139 .expect("submilli:http declares download")
3140 .clone();
3141 package.values.insert("fetch".to_string(), download);
3142
3143 let docs = render_packages_d_ts(&[&package], &[]);
3144
3145 assert!(docs.starts_with("declare module \"@pied-piper/files\" {\n"));
3146 assert!(docs.contains(" import type { DownloadResult } from \"submilli:http\";\n"));
3147 assert!(docs.contains(" import type { DownloadOptions } from \"submilli:http\";\n"));
3148 assert!(!docs.contains("import type { Response }"));
3149 }
3150
3151 #[test]
3152 fn a_borrowed_type_is_imported_from_its_own_module_when_others_share_its_name() {
3153 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3156 leaf.types
3157 .insert("Page".to_string(), alias_symbol("@pied-piper/leaf", "Page"));
3158 let mut other = PackageDeclaration::with_package("@pied-piper/other");
3159 other.types.insert(
3160 "Page".to_string(),
3161 alias_symbol("@pied-piper/other", "Page"),
3162 );
3163 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3164 let page = reference(
3165 "@pied-piper/leaf",
3166 crate::mangle::package_symbol("@pied-piper/leaf", "Page"),
3167 "Page",
3168 );
3169 mid.values.insert(
3170 "page".to_string(),
3171 const_symbol("@pied-piper/mid", "page", page),
3172 );
3173
3174 let docs = render_packages_d_ts(&[&leaf, &other, &mid], &[]);
3175
3176 assert!(docs.contains(" import type { Page } from \"@pied-piper/leaf\";\n"));
3177 assert!(!docs.contains("from \"submilli:session\""));
3178 assert!(!docs.contains("from \"@pied-piper/other\""));
3179 }
3180
3181 fn alias_symbol(package: &str, name: &str) -> crate::TypeSymbol {
3182 crate::TypeSymbol {
3183 name: name.to_string(),
3184 mangled_name: crate::mangle::package_symbol(package, name),
3185 declaration_span: crate::Span::at(crate::FileId(0)),
3186 kind: crate::TypeKind::Alias {
3187 generics: Vec::new(),
3188 ty: Type::Number,
3189 doc: None,
3190 },
3191 }
3192 }
3193
3194 fn const_symbol(package: &str, name: &str, ty: Type) -> ValueSymbol {
3195 ValueSymbol {
3196 name: name.to_string(),
3197 mangled_name: crate::mangle::package_symbol(package, name),
3198 declaration_span: crate::Span::at(crate::FileId(0)),
3199 kind: ValueKind::Const { ty, doc: None },
3200 }
3201 }
3202
3203 fn reference(package: &str, mangled: crate::MangledName, local_name: &str) -> Type {
3204 Type::alias_ref(
3205 crate::types::Package(package.to_string()),
3206 local_name,
3207 mangled,
3208 Vec::new(),
3209 )
3210 }
3211
3212 #[test]
3213 fn an_aliased_type_is_imported_under_the_name_the_text_uses() {
3214 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3215 leaf.types
3216 .insert("Page".to_string(), alias_symbol("@pied-piper/leaf", "Page"));
3217 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3218 let page = reference(
3219 "@pied-piper/leaf",
3220 crate::mangle::package_symbol("@pied-piper/leaf", "Page"),
3221 "LeafPage",
3222 );
3223 mid.values.insert(
3224 "page".to_string(),
3225 const_symbol("@pied-piper/mid", "page", page),
3226 );
3227
3228 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3229
3230 assert!(docs.contains(" import type { Page as LeafPage } from \"@pied-piper/leaf\";\n"));
3231 assert!(!docs.contains("declare module \"@pied-piper/leaf\""));
3232 }
3233
3234 #[test]
3235 fn a_type_reexported_from_an_internal_module_is_imported_from_the_package() {
3236 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3237 leaf.types.insert(
3238 "Status".to_string(),
3239 alias_symbol("@pied-piper/leaf", "Status"),
3240 );
3241 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3242 let status = reference(
3243 "@pied-piper/leaf",
3244 crate::mangle::package_module_symbol("@pied-piper/leaf", "model", "Status"),
3245 "Status",
3246 );
3247 mid.values.insert(
3248 "status".to_string(),
3249 const_symbol("@pied-piper/mid", "status", status),
3250 );
3251
3252 let docs = render_packages_d_ts(&[&leaf, &mid], &[]);
3253
3254 assert!(docs.contains(" import type { Status } from \"@pied-piper/leaf\";\n"));
3255 }
3256
3257 #[test]
3258 fn only_the_rendered_declarations_bring_imports() {
3259 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3260 let response = reference(
3261 "submilli:http",
3262 crate::mangle::package_symbol("submilli:http", "Response"),
3263 "Response",
3264 );
3265 mid.runtime_globals.insert(
3266 crate::mangle::package_symbol("@pied-piper/mid", "hidden"),
3267 response.clone(),
3268 );
3269
3270 let docs = render_packages_d_ts(&[&mid], &[]);
3271
3272 assert!(!docs.contains("import type"), "{docs}");
3273 }
3274
3275 #[test]
3276 fn a_value_named_like_a_borrowed_interface_leaves_its_import() {
3277 let download = reference(
3280 "submilli:http",
3281 crate::mangle::package_symbol("submilli:http", "DownloadResult"),
3282 "DownloadResult",
3283 );
3284 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3285 mid.values.insert(
3286 "DownloadResult".to_string(),
3287 const_symbol("@pied-piper/mid", "DownloadResult", download),
3288 );
3289
3290 let docs = render_packages_d_ts(&[&mid], &[]);
3291
3292 assert!(
3293 docs.contains(" import type { DownloadResult } from \"submilli:http\";\n"),
3294 "{docs}"
3295 );
3296 }
3297
3298 #[test]
3299 fn type_arguments_bring_imports_and_alias_bodies_do_not() {
3300 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3301 leaf.types
3302 .insert("Page".to_string(), alias_symbol("@pied-piper/leaf", "Page"));
3303 leaf.types
3304 .insert("Item".to_string(), alias_symbol("@pied-piper/leaf", "Item"));
3305 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3306 let item = reference(
3307 "@pied-piper/leaf",
3308 crate::mangle::package_symbol("@pied-piper/leaf", "Item"),
3309 "Item",
3310 );
3311 let page_of_items = Type::alias_ref(
3312 crate::types::Package("@pied-piper/leaf".to_string()),
3313 "Page",
3314 crate::mangle::package_symbol("@pied-piper/leaf", "Page"),
3315 vec![Type::Array(Box::new(item))],
3316 );
3317 mid.values.insert(
3318 "page".to_string(),
3319 const_symbol("@pied-piper/mid", "page", page_of_items),
3320 );
3321 let response = reference(
3324 "submilli:http",
3325 crate::mangle::package_symbol("submilli:http", "Response"),
3326 "Response",
3327 );
3328 let wrapped = Type::Alias {
3329 mangled: crate::mangle::package_symbol("@pied-piper/leaf", "Item"),
3330 package: crate::types::Package("@pied-piper/leaf".to_string()),
3331 name: "Item".to_string(),
3332 args: Vec::new(),
3333 ty: Box::new(response),
3334 };
3335 mid.values.insert(
3336 "item".to_string(),
3337 const_symbol("@pied-piper/mid", "item", wrapped),
3338 );
3339
3340 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3341
3342 assert!(docs.contains(" import type { Page } from \"@pied-piper/leaf\";\n"));
3343 assert!(docs.contains(" import type { Item } from \"@pied-piper/leaf\";\n"));
3344 assert!(!docs.contains("Response"), "{docs}");
3345 }
3346
3347 fn class_symbol(
3348 package: &str,
3349 name: &str,
3350 extends: Option<crate::ClassExtends>,
3351 ) -> crate::TypeSymbol {
3352 crate::TypeSymbol {
3353 name: name.to_string(),
3354 mangled_name: crate::mangle::package_symbol(package, name),
3355 declaration_span: crate::Span::at(crate::FileId(0)),
3356 kind: TypeKind::Class {
3357 generics: Vec::new(),
3358 fields: BTreeMap::new(),
3359 narrowing_checks: BTreeMap::new(),
3360 methods: BTreeMap::new(),
3361 method_visibility: BTreeMap::new(),
3362 accessors: Vec::new(),
3363 constructor: Vec::new(),
3364 constructor_visibility: crate::Visibility::Public,
3365 statics: BTreeMap::new(),
3366 static_visibility: BTreeMap::new(),
3367 static_fields: BTreeMap::new(),
3368 extends,
3369 implements: Vec::new(),
3370 doc: None,
3371 },
3372 }
3373 }
3374
3375 #[test]
3376 fn a_class_renders_and_imports_its_parent() {
3377 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3378 leaf.types.insert(
3379 "Base".to_string(),
3380 class_symbol("@pied-piper/leaf", "Base", None),
3381 );
3382 leaf.types
3383 .insert("Item".to_string(), alias_symbol("@pied-piper/leaf", "Item"));
3384 let item = reference(
3385 "@pied-piper/leaf",
3386 crate::mangle::package_symbol("@pied-piper/leaf", "Item"),
3387 "Item",
3388 );
3389 let extends = crate::ClassExtends {
3390 parent: crate::mangle::package_symbol("@pied-piper/leaf", "Base"),
3391 args: vec![item],
3392 };
3393 let mut sub = class_symbol("@pied-piper/mid", "Sub", Some(extends));
3394 if let TypeKind::Class { fields, .. } = &mut sub.kind {
3395 let response = reference(
3397 "submilli:http",
3398 crate::mangle::package_symbol("submilli:http", "Response"),
3399 "Response",
3400 );
3401 fields.insert(
3402 "hidden".to_string(),
3403 crate::FieldSig {
3404 ty: response,
3405 visibility: crate::Visibility::Private,
3406 readonly: false,
3407 optional: false,
3408 method: false,
3409 doc: None,
3410 },
3411 );
3412 }
3413 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3414 mid.types.insert("Sub".to_string(), sub);
3415
3416 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3417
3418 assert!(
3419 docs.contains(" import type { Base } from \"@pied-piper/leaf\";\n"),
3420 "{docs}"
3421 );
3422 assert!(
3423 docs.contains(" import type { Item } from \"@pied-piper/leaf\";\n"),
3424 "{docs}"
3425 );
3426 assert!(
3427 docs.contains(" export class Sub extends Base<Item> {"),
3428 "{docs}"
3429 );
3430 assert!(!docs.contains("Response"), "{docs}");
3431 }
3432
3433 fn subclass_of(package: &str, name: &str, parent: crate::MangledName) -> crate::TypeSymbol {
3434 class_symbol(package, name, Some(crate::ClassExtends::plain(parent)))
3435 }
3436
3437 fn two_bases() -> (PackageDeclaration, PackageDeclaration) {
3439 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3440 leaf.types.insert(
3441 "Base".to_string(),
3442 class_symbol("@pied-piper/leaf", "Base", None),
3443 );
3444 let mut other = PackageDeclaration::with_package("@pied-piper/other");
3445 other.types.insert(
3446 "Base".to_string(),
3447 class_symbol("@pied-piper/other", "Base", None),
3448 );
3449 (leaf, other)
3450 }
3451
3452 #[test]
3453 fn a_parent_named_like_a_local_value_is_imported_under_another_name() {
3454 let (leaf, _) = two_bases();
3455 let mut package = PackageDeclaration::with_package("@pied-piper/shadowed");
3456 package.values.insert(
3457 "Base".to_string(),
3458 const_symbol("@pied-piper/shadowed", "Base", Type::Number),
3459 );
3460 let base = crate::mangle::package_symbol("@pied-piper/leaf", "Base");
3461 package.types.insert(
3462 "Sub".to_string(),
3463 subclass_of("@pied-piper/shadowed", "Sub", base),
3464 );
3465
3466 let docs = render_packages_d_ts(&[&package], &[&leaf]);
3467
3468 assert!(
3469 docs.contains(" import type { Base as Base1 } from \"@pied-piper/leaf\";\n"),
3470 "{docs}"
3471 );
3472 assert!(
3473 docs.contains(" export class Sub extends Base1 {"),
3474 "{docs}"
3475 );
3476 }
3477
3478 #[test]
3479 fn parents_that_share_a_public_name_get_different_local_names() {
3480 let (leaf, other) = two_bases();
3481 let mut package = PackageDeclaration::with_package("@pied-piper/both");
3482 package.types.insert(
3483 "A".to_string(),
3484 subclass_of(
3485 "@pied-piper/both",
3486 "A",
3487 crate::mangle::package_symbol("@pied-piper/leaf", "Base"),
3488 ),
3489 );
3490 package.types.insert(
3491 "B".to_string(),
3492 subclass_of(
3493 "@pied-piper/both",
3494 "B",
3495 crate::mangle::package_symbol("@pied-piper/other", "Base"),
3496 ),
3497 );
3498
3499 let docs = render_packages_d_ts(&[&package], &[&leaf, &other]);
3500
3501 assert!(
3502 docs.contains(" import type { Base } from \"@pied-piper/leaf\";\n"),
3503 "{docs}"
3504 );
3505 assert!(
3506 docs.contains(" import type { Base as Base1 } from \"@pied-piper/other\";\n"),
3507 "{docs}"
3508 );
3509 assert!(docs.contains(" export class A extends Base {"), "{docs}");
3510 assert!(docs.contains(" export class B extends Base1 {"), "{docs}");
3511 }
3512
3513 #[test]
3514 fn a_parent_already_imported_under_an_alias_keeps_it() {
3515 let (leaf, _) = two_bases();
3516 let base = crate::mangle::package_symbol("@pied-piper/leaf", "Base");
3517 let mut package = PackageDeclaration::with_package("@pied-piper/aliased");
3518 package.values.insert(
3519 "base".to_string(),
3520 const_symbol(
3521 "@pied-piper/aliased",
3522 "base",
3523 reference("@pied-piper/leaf", base.clone(), "LB"),
3524 ),
3525 );
3526 package.types.insert(
3527 "Sub".to_string(),
3528 subclass_of("@pied-piper/aliased", "Sub", base),
3529 );
3530
3531 let docs = render_packages_d_ts(&[&package], &[&leaf]);
3532
3533 assert!(
3534 docs.contains(" import type { Base as LB } from \"@pied-piper/leaf\";\n"),
3535 "{docs}"
3536 );
3537 assert!(docs.contains(" export class Sub extends LB {"), "{docs}");
3538 assert!(!docs.contains("import type { Base }"), "{docs}");
3539 }
3540
3541 #[test]
3542 fn a_class_extending_a_builtin_error_renders_its_parent_as_the_global() {
3543 let error = builtin_package_declaration()
3544 .types
3545 .get("Error")
3546 .expect("the prelude declares Error")
3547 .mangled_name
3548 .clone();
3549 let mut package = PackageDeclaration::with_package("@pied-piper/errors");
3550 package.types.insert(
3551 "BillingError".to_string(),
3552 subclass_of("@pied-piper/errors", "BillingError", error),
3553 );
3554
3555 let docs = render_packages_d_ts(&[&package], &[]);
3556
3557 assert!(
3558 docs.contains(" export class BillingError extends Error {"),
3559 "{docs}"
3560 );
3561 assert!(!docs.contains("import type"), "{docs}");
3562 }
3563
3564 #[test]
3565 fn a_package_value_named_like_the_global_parent_leaves_the_class_without_it() {
3566 let error = builtin_package_declaration()
3567 .types
3568 .get("Error")
3569 .expect("the prelude declares Error")
3570 .mangled_name
3571 .clone();
3572 let mut package = PackageDeclaration::with_package("@pied-piper/errors");
3573 package.values.insert(
3574 "Error".to_string(),
3575 const_symbol("@pied-piper/errors", "Error", Type::Number),
3576 );
3577 package.types.insert(
3578 "BillingError".to_string(),
3579 subclass_of("@pied-piper/errors", "BillingError", error),
3580 );
3581
3582 let docs = render_packages_d_ts(&[&package], &[]);
3583
3584 assert!(docs.contains(" export class BillingError {"), "{docs}");
3585 }
3586
3587 #[test]
3588 fn global_functions_are_in_the_editor_declarations() {
3589 let lib = render_lib_submilli_d_ts();
3590 for function in [
3591 "parseInt(",
3592 "parseFloat(",
3593 "encodeURIComponent(",
3594 "encodeURI(",
3595 "decodeURIComponent(",
3596 "decodeURI(",
3597 "isNaN(",
3598 "isFinite(",
3599 ] {
3600 assert!(
3601 lib.contains(&format!("declare function {function}")),
3602 "{function}"
3603 );
3604 }
3605 }
3606
3607 #[test]
3608 fn builtin_errors_extend_error_in_the_editor_declarations() {
3609 let lib = render_lib_submilli_d_ts();
3610 for class in [
3611 "QuotaExceededError",
3612 "RangeError",
3613 "TypeError",
3614 "SyntaxError",
3615 "URIError",
3616 "ReferenceError",
3617 "PermissionDeniedError",
3618 ] {
3619 assert!(
3620 lib.contains(&format!("declare class {class} extends Error {{")),
3621 "{class}"
3622 );
3623 }
3624 assert!(lib.contains("declare class Error {"));
3625 }
3626
3627 #[test]
3628 fn a_value_named_like_a_borrowed_class_keeps_the_class_out() {
3629 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3631 leaf.types.insert(
3632 "Base".to_string(),
3633 class_symbol("@pied-piper/leaf", "Base", None),
3634 );
3635 let base = Type::class_ref(
3636 crate::types::Package("@pied-piper/leaf".to_string()),
3637 "Base",
3638 crate::mangle::package_symbol("@pied-piper/leaf", "Base"),
3639 Vec::new(),
3640 );
3641 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3642 mid.values.insert(
3643 "Base".to_string(),
3644 const_symbol("@pied-piper/mid", "Base", base),
3645 );
3646
3647 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3648
3649 assert!(!docs.contains("import type"), "{docs}");
3650 }
3651
3652 #[test]
3653 fn a_package_type_named_like_a_constructor_is_an_ordinary_type() {
3654 let mut package = PackageDeclaration::with_package("@pied-piper/leaf");
3655 package.types.insert(
3656 "Client".to_string(),
3657 alias_symbol("@pied-piper/leaf", "Client"),
3658 );
3659 package.types.insert(
3660 "ClientConstructor".to_string(),
3661 alias_symbol("@pied-piper/leaf", "ClientConstructor"),
3662 );
3663
3664 let docs = render_packages_d_ts(&[&package], &[]);
3665
3666 assert!(docs.contains(" export type Client = number;"), "{docs}");
3667 assert!(
3668 docs.contains(" export type ClientConstructor = number;"),
3669 "{docs}"
3670 );
3671 assert!(!docs.contains("const Client"), "{docs}");
3672 }
3673
3674 #[test]
3675 fn stdlib_d_ts_declares_each_user_module() {
3676 let docs = every_package().render_stdlib_d_ts();
3677 for module in user_modules(every_package()) {
3678 assert!(
3679 docs.contains(&format!("declare module \"{}\" {{", module.package_name)),
3680 "missing declare module for {}",
3681 module.package_name
3682 );
3683 }
3684 assert!(docs.contains("declare module \"submilli:security\" {\n"));
3685 assert!(docs.contains(" export function check<T>("));
3686 assert!(docs.contains("declare module \"submilli:uuid\" {\n"));
3687 assert!(docs.contains(" export function v4("));
3688 assert!(docs.contains(" function delete_("));
3689 assert!(docs.contains(" export { delete_ as delete };"));
3690 assert!(!docs.contains("export function delete("));
3691 }
3692
3693 #[test]
3694 fn lib_submilli_d_ts_renders_ambient_builtins() {
3695 let docs = render_lib_submilli_d_ts();
3696 assert!(docs.contains("interface Array<"));
3697 assert!(docs.contains("interface ArrayConstructor"));
3698 assert!(docs.contains("declare const Array: ArrayConstructor;"));
3699 assert_eq!(
3700 docs.matches("declare const Array: ArrayConstructor;")
3701 .count(),
3702 1
3703 );
3704 assert!(docs.contains("declare const console: Console;"));
3705 assert!(docs.contains("declare namespace Math {"));
3706 assert!(docs.contains("declare namespace Temporal {"));
3707 assert!(docs.contains("declare namespace JSON {"));
3708 assert!(docs.contains(
3709 "function stringify<T>(value: T, replacer?: null, space?: number | string | null): string | undefined;"
3710 ));
3711 assert!(docs.contains("interface Function {\n /** The parameters before the first optional, defaulted or rest one. */\n readonly length: number;\n}"));
3712 assert!(docs.contains("interface IArguments {}"));
3713 assert!(docs.contains("declare class Error {"));
3714 assert!(docs.contains("static isError(value?: unknown): value is Error;"));
3715 assert!(!docs.contains("ErrorConstructor"));
3716 assert!(!docs.contains("declare module"));
3717 assert!(!docs.contains("function string_concat"));
3718 }
3719
3720 #[test]
3721 fn d_ts_renderer_uses_typescript_call_and_predicate_signatures() {
3722 let docs = render_lib_submilli_d_ts();
3723 assert!(docs.contains("(value: string | bigint | undefined): number;"));
3724 assert!(docs.contains("isArray<T>(value: T): value is T & unknown[];"));
3725 assert!(!docs.contains("@call"));
3726 }
3727
3728 fn class_with_constructor(
3729 package: &str,
3730 name: &str,
3731 constructor: Vec<Param>,
3732 constructor_visibility: crate::Visibility,
3733 extends: Option<ClassExtends>,
3734 ) -> TypeSymbol {
3735 TypeSymbol {
3736 name: name.to_string(),
3737 mangled_name: crate::mangle::package_symbol(package, name),
3738 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3739 kind: TypeKind::Class {
3740 generics: Vec::new(),
3741 fields: BTreeMap::new(),
3742 narrowing_checks: BTreeMap::new(),
3743 methods: BTreeMap::new(),
3744 method_visibility: BTreeMap::new(),
3745 accessors: Vec::new(),
3746 constructor,
3747 constructor_visibility,
3748 statics: BTreeMap::new(),
3749 static_visibility: BTreeMap::new(),
3750 static_fields: BTreeMap::new(),
3751 extends,
3752 implements: Vec::new(),
3753 doc: None,
3754 },
3755 }
3756 }
3757
3758 #[test]
3759 fn a_private_constructor_stays_out_of_the_docs_and_is_protected_in_d_ts() {
3760 let package = "@pied-piper/shapes";
3761 let mut defs = PackageDeclaration::with_package(package);
3762 let point = class_with_constructor(
3763 package,
3764 "Point",
3765 vec![Param::new("secret", Type::String)],
3766 crate::Visibility::Private,
3767 None,
3768 );
3769 let labeled = class_with_constructor(
3770 package,
3771 "Labeled",
3772 vec![Param::new("secret", Type::String)],
3773 crate::Visibility::Private,
3774 Some(ClassExtends {
3775 parent: point.mangled_name.clone(),
3776 args: Vec::new(),
3777 }),
3778 );
3779 defs.types.insert("Point".to_string(), point);
3780 defs.types.insert("Labeled".to_string(), labeled);
3781
3782 let docs = render_declarations(&defs);
3783 assert!(docs.contains("class Point"), "{docs}");
3784 assert!(!docs.contains("constructor"), "{docs}");
3785
3786 let d_ts = render_packages_d_ts(&[&defs], &[]);
3789 assert!(d_ts.contains("class Labeled extends Point"), "{d_ts}");
3790 assert_eq!(
3791 d_ts.matches("protected constructor();").count(),
3792 2,
3793 "{d_ts}"
3794 );
3795 assert!(!d_ts.contains("secret"), "{d_ts}");
3796 }
3797
3798 #[test]
3799 fn constructor_visibility_round_trips_and_a_public_one_is_not_written() {
3800 let package = "@pied-piper/shapes";
3801 for visibility in [crate::Visibility::Private, crate::Visibility::Public] {
3802 let symbol = class_with_constructor(package, "Point", Vec::new(), visibility, None);
3803 let json = serde_json::to_string(&symbol.kind).unwrap();
3804 assert_eq!(
3805 json.contains("constructor_visibility"),
3806 !visibility.is_public()
3807 );
3808 let TypeKind::Class {
3809 constructor_visibility,
3810 ..
3811 } = serde_json::from_str::<TypeKind>(&json).unwrap()
3812 else {
3813 panic!("not a class: {json}");
3814 };
3815 assert_eq!(constructor_visibility, visibility);
3816 }
3817 }
3818
3819 #[test]
3820 fn class_declarations_omit_private_members() {
3821 use crate::{FieldSig, MethodSig, Visibility};
3822
3823 let field = |ty, visibility| FieldSig {
3824 ty,
3825 visibility,
3826 readonly: false,
3827 optional: false,
3828 method: false,
3829 doc: None,
3830 };
3831 let method = |ret| MethodSig {
3832 optional: false,
3833 generics: Vec::new(),
3834 params: Vec::new(),
3835 ret,
3836 predicate: None,
3837 doc: None,
3838 };
3839
3840 let mut fields = BTreeMap::new();
3841 fields.insert("name".to_string(), field(Type::String, Visibility::Public));
3842 fields.insert(
3843 "sound".to_string(),
3844 field(Type::String, Visibility::Private),
3845 );
3846 let mut methods = BTreeMap::new();
3847 methods.insert("speak".to_string(), method(Type::String));
3848 methods.insert("secret".to_string(), method(Type::Number));
3849 let mut method_visibility = BTreeMap::new();
3850 method_visibility.insert("speak".to_string(), Visibility::Public);
3851 method_visibility.insert("secret".to_string(), Visibility::Private);
3852
3853 let mut defs = PackageDeclaration::with_package("@pied-piper/zoo");
3854 defs.types.insert(
3855 "Animal".to_string(),
3856 TypeSymbol {
3857 name: "Animal".to_string(),
3858 mangled_name: crate::mangle::package_symbol("@pied-piper/zoo", "Animal"),
3859 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3860 kind: TypeKind::Class {
3861 generics: Vec::new(),
3862 fields,
3863 narrowing_checks: BTreeMap::new(),
3864 methods,
3865 method_visibility,
3866 accessors: Vec::new(),
3867 constructor: vec![Param::new("name", Type::String)],
3868 constructor_visibility: crate::Visibility::Public,
3869 statics: BTreeMap::new(),
3870 static_visibility: BTreeMap::new(),
3871 static_fields: BTreeMap::new(),
3872 extends: None,
3873 implements: Vec::new(),
3874 doc: None,
3875 },
3876 },
3877 );
3878
3879 let rendered = render_declarations(&defs);
3880 assert!(rendered.contains("class Animal {"), "{rendered}");
3882 assert!(rendered.contains("name: string;"), "{rendered}");
3883 assert!(rendered.contains("speak(): string;"), "{rendered}");
3884 assert!(
3885 rendered.contains("constructor(name: string);"),
3886 "{rendered}"
3887 );
3888 assert!(!rendered.contains("private "), "{rendered}");
3890 assert!(!rendered.contains("sound"), "{rendered}");
3891 assert!(!rendered.contains("secret"), "{rendered}");
3892 }
3893
3894 #[test]
3895 fn class_declarations_render_accessors_faithfully() {
3896 use crate::{AccessorSig, FieldSig, Param, Visibility};
3897
3898 let prop = |ty: Type, readonly| FieldSig {
3901 ty,
3902 visibility: Visibility::Public,
3903 readonly,
3904 optional: false,
3905 method: false,
3906 doc: None,
3907 };
3908 let mut fields = BTreeMap::new();
3909 fields.insert("area".to_string(), prop(Type::Number, true)); fields.insert("size".to_string(), prop(Type::Number, false)); fields.insert("label".to_string(), prop(Type::String, false)); fields.insert("secret".to_string(), prop(Type::Number, false)); let setter = |name: &str, ty: Type| AccessorSig::Setter {
3915 name: name.to_string(),
3916 param: Param::new("v", ty),
3917 };
3918 let getter = |name: &str, ty: Type| AccessorSig::Getter {
3919 name: name.to_string(),
3920 ret_ty: ty,
3921 };
3922 let accessors = vec![
3923 getter("area", Type::Number),
3924 getter("size", Type::Number),
3925 setter("size", Type::Number),
3926 getter("label", Type::String),
3927 setter("label", Type::Number),
3928 setter("secret", Type::Number),
3929 ];
3930
3931 let mut defs = PackageDeclaration::with_package("@pied-piper/geo");
3932 defs.types.insert(
3933 "Shape".to_string(),
3934 TypeSymbol {
3935 name: "Shape".to_string(),
3936 mangled_name: crate::mangle::package_symbol("@pied-piper/geo", "Shape"),
3937 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3938 kind: TypeKind::Class {
3939 generics: Vec::new(),
3940 fields,
3941 narrowing_checks: BTreeMap::new(),
3942 methods: BTreeMap::new(),
3943 method_visibility: BTreeMap::new(),
3944 accessors,
3945 constructor: Vec::new(),
3946 constructor_visibility: crate::Visibility::Public,
3947 statics: BTreeMap::new(),
3948 static_visibility: BTreeMap::new(),
3949 static_fields: BTreeMap::new(),
3950 extends: None,
3951 implements: Vec::new(),
3952 doc: None,
3953 },
3954 },
3955 );
3956
3957 let docs = render_declarations(&defs);
3958 let shape = &defs.types.get("Shape").unwrap().kind;
3959 let mut dts = String::new();
3960 render_ts_type(&mut dts, "Shape", shape, "", "export ", None);
3961 for rendered in [&docs, &dts] {
3962 assert!(rendered.contains("readonly area: number;"), "{rendered}"); assert!(rendered.contains("size: number;"), "{rendered}"); assert!(rendered.contains("get label(): string;"), "{rendered}"); assert!(rendered.contains("set label(v: number);"), "{rendered}");
3966 assert!(rendered.contains("set secret(v: number);"), "{rendered}"); assert!(!rendered.contains("get area"), "{rendered}");
3969 assert!(!rendered.contains("get size("), "{rendered}");
3970 }
3971 }
3972
3973 #[test]
3974 fn class_declarations_render_public_statics_only() {
3975 use crate::{FieldSig, MethodSig, Param, Visibility};
3976
3977 let method = |ret: Type| MethodSig {
3978 optional: false,
3979 generics: Vec::new(),
3980 params: vec![Param::new("x", Type::Number)],
3981 ret,
3982 predicate: None,
3983 doc: None,
3984 };
3985 let mut statics = BTreeMap::new();
3986 statics.insert("make".to_string(), method(Type::Number));
3987 statics.insert("hidden".to_string(), method(Type::Number));
3988 let mut static_visibility = BTreeMap::new();
3989 static_visibility.insert("make".to_string(), Visibility::Public);
3990 static_visibility.insert("hidden".to_string(), Visibility::Private);
3991 let mut static_fields = BTreeMap::new();
3992 static_fields.insert(
3993 "MAX".to_string(),
3994 FieldSig {
3995 ty: Type::Number,
3996 visibility: Visibility::Public,
3997 readonly: true,
3998 optional: false,
3999 method: false,
4000 doc: None,
4001 },
4002 );
4003 static_fields.insert(
4004 "KEY".to_string(),
4005 FieldSig {
4006 ty: Type::Number,
4007 visibility: Visibility::Private,
4008 readonly: true,
4009 optional: false,
4010 method: false,
4011 doc: None,
4012 },
4013 );
4014 static_fields.insert(
4015 "count".to_string(),
4016 FieldSig {
4017 ty: Type::Number,
4018 visibility: Visibility::Public,
4019 readonly: false,
4020 optional: false,
4021 method: false,
4022 doc: None,
4023 },
4024 );
4025 static_fields.insert(
4026 "seed".to_string(),
4027 FieldSig {
4028 ty: Type::Number,
4029 visibility: Visibility::Private,
4030 readonly: false,
4031 optional: false,
4032 method: false,
4033 doc: None,
4034 },
4035 );
4036
4037 let mut defs = PackageDeclaration::with_package("@pied-piper/calc");
4038 defs.types.insert(
4039 "Calc".to_string(),
4040 TypeSymbol {
4041 name: "Calc".to_string(),
4042 mangled_name: crate::mangle::package_symbol("@pied-piper/calc", "Calc"),
4043 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
4044 kind: TypeKind::Class {
4045 generics: Vec::new(),
4046 fields: BTreeMap::new(),
4047 narrowing_checks: BTreeMap::new(),
4048 methods: BTreeMap::new(),
4049 method_visibility: BTreeMap::new(),
4050 accessors: Vec::new(),
4051 constructor: Vec::new(),
4052 constructor_visibility: crate::Visibility::Public,
4053 statics,
4054 static_visibility,
4055 static_fields,
4056 extends: None,
4057 implements: Vec::new(),
4058 doc: None,
4059 },
4060 },
4061 );
4062
4063 let docs = render_declarations(&defs);
4064 let calc = &defs.types.get("Calc").unwrap().kind;
4065 let mut dts = String::new();
4066 render_ts_type(&mut dts, "Calc", calc, "", "export ", None);
4067 for rendered in [&docs, &dts] {
4068 assert!(
4069 rendered.contains("static make(x: number): number;"),
4070 "{rendered}"
4071 );
4072 assert!(
4073 rendered.contains("static readonly MAX: number;"),
4074 "{rendered}"
4075 );
4076 assert!(rendered.contains("static count: number;"), "{rendered}");
4077 assert!(!rendered.contains("hidden"), "{rendered}");
4078 assert!(!rendered.contains("KEY"), "{rendered}");
4079 assert!(!rendered.contains("seed"), "{rendered}");
4080 }
4081 }
4082
4083 #[test]
4084 fn a_type_re_exported_under_another_name_renders_by_that_name() {
4085 let model = r#"
4086 export interface Res { ok: boolean; }
4087 export type Alias = string | Res;
4088 export class Thing { name: string = "t"; }
4089 export interface Both { n: number; }
4090 "#;
4091 let lib = r#"
4092 export { Res as Result, Alias as Either, Thing as Item, Both, Both as Pair } from "./model";
4093 import { Res, Alias, Thing, Both } from "./model";
4094 /** Get one. @returns the result */
4095 export function read(): Res { return { ok: true }; }
4096 /** Use one. @param a the value */
4097 export function either(a: Alias): void { }
4098 /** Make one. @param r a result @returns the thing */
4099 export function make(r: Res): Thing { return new Thing(); }
4100 /** A pair. @returns it */
4101 export function pair(): Both { return { n: 1 }; }
4102 /** A list of either. */
4103 export type Wrapped = Res[] | Thing;
4104 "#;
4105 let defs = crate::compile_package(
4106 "@pied-piper/renamed",
4107 crate::ModulePath::from("lib"),
4108 &[
4109 crate::PackageSourceModule {
4110 path: crate::ModulePath::from("lib"),
4111 source: lib,
4112 },
4113 crate::PackageSourceModule {
4114 path: crate::ModulePath::from("model"),
4115 source: model,
4116 },
4117 ],
4118 &[],
4119 )
4120 .unwrap_or_else(|diagnostics| panic!("compile failed: {diagnostics:#?}"))
4121 .declaration;
4122
4123 let docs = render_declarations(&defs);
4124 let d_ts = render_packages_d_ts(&[&defs], &[]);
4125 for rendered in [&docs, &d_ts] {
4126 assert!(rendered.contains("read(): Result;"), "{rendered}");
4127 assert!(rendered.contains("either(a: Either): void;"), "{rendered}");
4128 assert!(rendered.contains("make(r: Result): Item;"), "{rendered}");
4129 assert!(
4130 rendered.contains("type Either = string | Result;"),
4131 "{rendered}"
4132 );
4133 assert!(
4134 rendered.contains("type Wrapped = Result[] | Item;"),
4135 "{rendered}"
4136 );
4137 assert!(rendered.contains("pair(): Both;"), "{rendered}");
4139 for source_name in ["Res", "Alias", "Thing"] {
4140 assert!(
4141 !rendered.contains(&format!("{source_name};"))
4142 && !rendered.contains(&format!("{source_name}[]"))
4143 && !rendered.contains(&format!("{source_name})")),
4144 "{source_name} in {rendered}"
4145 );
4146 }
4147 }
4148 }
4149}