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