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