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
35#[derive(Clone, Copy, PartialEq, Eq)]
38pub enum DocAudience {
39 Author,
40 Agent,
41}
42
43pub struct ModuleSummary {
45 pub name: String,
46 pub description: String,
47}
48
49pub fn docs(name: &str) -> Option<ModuleDoc> {
53 Stdlib::core().docs(name)
54}
55
56pub fn search(query: &str) -> Vec<ModuleSummary> {
59 Stdlib::core().search(query)
60}
61
62impl Stdlib {
65 pub fn view(self, availability: &dyn DocAvailability) -> StdlibView {
69 self.view_where(availability, |_, usable| usable)
70 }
71
72 pub fn view_where(
76 self,
77 availability: &dyn DocAvailability,
78 keep: impl Fn(&str, bool) -> bool,
79 ) -> StdlibView {
80 let modules = user_modules(self)
81 .iter()
82 .filter_map(|defs| {
83 let projection = project_declaration(defs, availability);
84 keep(&defs.package_name, projection.usable).then_some(projection.declaration)
85 })
86 .collect();
87 StdlibView { modules }
88 }
89
90 pub fn docs(self, name: &str) -> Option<ModuleDoc> {
92 self.view(&Unscoped).docs(name)
93 }
94
95 pub fn search(self, query: &str) -> Vec<ModuleSummary> {
97 self.view(&Unscoped).search(query)
98 }
99
100 pub fn resolve(self, name: &str) -> Resolution {
102 self.view(&Unscoped).resolve(name)
103 }
104
105 pub fn suggest_filtered(
107 self,
108 name: &str,
109 extra: &[String],
110 visible: impl Fn(&str) -> bool,
111 ) -> Option<String> {
112 self.view(&Unscoped).suggest_filtered(name, extra, visible)
113 }
114
115 pub fn catalog_filtered(
117 self,
118 extra: Vec<CatalogEntry>,
119 visible: impl Fn(&str) -> bool,
120 ) -> Catalog {
121 self.view(&Unscoped).catalog_filtered(extra, visible)
122 }
123
124 pub fn render_stdlib_d_ts(self) -> String {
126 let mut out = String::new();
127 for defs in &editor_stdlib_modules(self) {
128 render_declare_module(&mut out, defs);
129 }
130 out.trim_end().to_string()
131 }
132
133 pub fn render_packages_d_ts(
135 self,
136 packages: &[&PackageDeclaration],
137 context: &[&PackageDeclaration],
138 ) -> String {
139 let stdlib = editor_stdlib_modules(self);
140 let exporters = TypeExporters::new(
141 stdlib
142 .iter()
143 .chain(packages.iter().copied())
144 .chain(context.iter().copied()),
145 );
146 let globals = global_classes();
147 let mut out = String::new();
148 for defs in packages {
149 let defs = with_public_type_names(defs);
150 let defs = defs.as_ref();
151 let _ = writeln!(out, "declare module \"{}\" {{", defs.package_name);
152 let imports = block_imports(defs, &exporters);
153 for (local, (public, module)) in &imports.types {
154 if local == public {
155 let _ = writeln!(out, " import type {{ {local} }} from \"{module}\";");
156 } else {
157 let _ = writeln!(
158 out,
159 " import type {{ {public} as {local} }} from \"{module}\";"
160 );
161 }
162 }
163 let parent = |extends: &ClassExtends| {
164 if let Some(local) = imports.parents.get(extends.parent.as_str()) {
165 return Some(ts_named_type(local, &extends.args));
166 }
167 let global = globals.get(extends.parent.as_str())?;
170 if declares(defs, global, true) {
171 return None;
172 }
173 Some(ts_named_type(global, &extends.args))
174 };
175 render_ts_declarations(&mut out, defs, " ", "export ", &parent);
176 let _ = writeln!(out, "}}\n");
177 }
178 out.trim_end().to_string()
179 }
180}
181
182pub struct StdlibView {
186 modules: Vec<PackageDeclaration>,
187}
188
189impl StdlibView {
190 pub fn names(&self) -> impl Iterator<Item = &str> {
192 self.modules.iter().map(|defs| defs.package_name.as_str())
193 }
194
195 pub fn contains(&self, name: &str) -> bool {
197 self.declaration(name).is_some()
198 }
199
200 pub fn declaration(&self, name: &str) -> Option<&PackageDeclaration> {
202 self.modules.iter().find(|defs| defs.package_name == name)
203 }
204
205 pub fn docs(&self, name: &str) -> Option<ModuleDoc> {
207 self.docs_for(name, DocAudience::Author)
208 }
209
210 pub fn docs_for(&self, name: &str, audience: DocAudience) -> Option<ModuleDoc> {
212 self.declaration(name).map(|defs| ModuleDoc {
213 name: defs.package_name.clone(),
214 description: module_description(&defs.package_name).to_string(),
215 declarations: render_declarations_for(defs, audience),
216 })
217 }
218
219 pub fn search(&self, query: &str) -> Vec<ModuleSummary> {
221 let q = query.trim().to_lowercase();
222 self.modules
223 .iter()
224 .filter(|defs| matches_query(defs, &q))
225 .map(|defs| ModuleSummary {
226 name: defs.package_name.clone(),
227 description: module_description(&defs.package_name).to_string(),
228 })
229 .collect()
230 }
231
232 pub fn resolve(&self, name: &str) -> Resolution {
234 if let Some(doc) = self.docs(name) {
235 return Resolution::Module(doc);
236 }
237 match builtin_lookup(name) {
238 BuiltinLookup::Found(declarations) => Resolution::Builtin {
239 name: name.to_string(),
240 declarations,
241 },
242 BuiltinLookup::UnknownMember {
243 path,
244 member,
245 members,
246 } => Resolution::UnknownMember {
247 path,
248 member,
249 members,
250 },
251 BuiltinLookup::Unknown => Resolution::Unknown,
252 }
253 }
254
255 pub fn suggest_filtered(
257 &self,
258 name: &str,
259 extra: &[String],
260 visible: impl Fn(&str) -> bool,
261 ) -> Option<String> {
262 let mut candidates: Vec<String> = self.search("").into_iter().map(|m| m.name).collect();
263 let builtins = builtins();
264 candidates.extend(builtins.types);
265 candidates.extend(builtins.namespaces);
266 candidates.extend(extra.iter().cloned());
267 candidates.retain(|candidate| visible(candidate));
268
269 let query = name.split('.').find(|s| !s.is_empty()).unwrap_or(name);
272
273 if let Some((_, replacement)) = OMITTED_GLOBALS
274 .iter()
275 .find(|(omitted, _)| omitted.eq_ignore_ascii_case(name))
276 {
277 return Some((*replacement).to_string());
278 }
279
280 let is_self = |c: &String| c.eq_ignore_ascii_case(name);
283 if let Some(hit) = candidates
286 .iter()
287 .find(|c| !is_self(c) && name_tail(c).eq_ignore_ascii_case(query))
288 {
289 return Some(hit.clone());
290 }
291 if query.len() < 3 {
295 return None;
296 }
297 let mut keys: Vec<(&str, &str)> = Vec::new();
301 for c in candidates.iter().filter(|c| !is_self(c)) {
302 keys.push((c.as_str(), c.as_str()));
303 let tail = name_tail(c);
304 if tail != c.as_str() {
305 keys.push((tail, c.as_str()));
306 }
307 }
308 let hit = crate::did_you_mean::closest_match(query, keys.iter().map(|(k, _)| *k))?;
309 keys.iter()
310 .find(|(k, _)| *k == hit)
311 .map(|(_, owner)| (*owner).to_string())
312 }
313
314 pub fn catalog_filtered(
316 &self,
317 extra: Vec<CatalogEntry>,
318 visible: impl Fn(&str) -> bool,
319 ) -> Catalog {
320 let mut entries: Vec<CatalogEntry> = self
321 .search("")
322 .into_iter()
323 .map(|m| CatalogEntry {
324 name: m.name,
325 source: SOURCE_STDLIB.to_string(),
326 description: m.description,
327 })
328 .collect();
329 entries.extend(extra);
330 entries.retain(|entry| visible(&entry.name));
331 let remaining = entries.len().saturating_sub(CATALOG_LIMIT);
332 entries.truncate(CATALOG_LIMIT);
333 Catalog { entries, remaining }
334 }
335
336 pub fn match_bare_name(&self, name: &str, extra: &[String]) -> BareNameMatch {
345 self.match_bare_name_for(name, extra, DocAudience::Author)
346 }
347
348 pub fn match_bare_name_for(
350 &self,
351 name: &str,
352 extra: &[String],
353 audience: DocAudience,
354 ) -> BareNameMatch {
355 if !is_bare_name(name) {
356 return BareNameMatch::Miss;
357 }
358 let has_tail = |candidate: &str| name_tail(candidate).eq_ignore_ascii_case(name);
359 let modules: Vec<String> = self
360 .names()
361 .filter(|module| has_tail(module))
362 .map(str::to_string)
363 .collect();
364 if modules.is_empty() {
365 return BareNameMatch::Miss;
368 }
369 let mut names = modules.clone();
370 names.extend(
374 extra
375 .iter()
376 .filter(|candidate| {
377 has_tail(candidate)
378 && !modules
379 .iter()
380 .any(|module| module.eq_ignore_ascii_case(candidate))
381 })
382 .cloned(),
383 );
384 names.sort();
385 names.dedup();
386 match names.as_slice() {
387 [module] => self
388 .docs_for(module, audience)
389 .map_or(BareNameMatch::Miss, BareNameMatch::Unique),
390 _ => BareNameMatch::Ambiguous(names),
391 }
392 }
393
394 pub fn has_bare_tail(&self, name: &str) -> bool {
397 is_bare_name(name)
398 && self
399 .names()
400 .any(|module| name_tail(module).eq_ignore_ascii_case(name))
401 }
402}
403
404pub enum BareNameMatch {
406 Miss,
408 Unique(ModuleDoc),
410 Ambiguous(Vec<String>),
412}
413
414fn is_bare_name(name: &str) -> bool {
417 !name.is_empty() && !name.contains([':', '@', '/', '.', '\\', ' '])
418}
419
420pub struct Builtins {
425 pub types: Vec<String>,
426 pub namespaces: Vec<String>,
427}
428
429const JSON_BUILTIN: &str = "JSON";
432
433const SUPPORTING_TYPES: &[&str] = &[
437 "Console",
438 "Base64Options",
439 "RegExpMatchArray",
440 "RegExpExecArray",
441 "Iterator",
442 "Iterable",
443 "IteratorResult",
444 "IteratorYieldResult",
445 "IteratorReturnResult",
446];
447
448fn is_headline_type(name: &str) -> bool {
449 !name.contains('#') && !name.ends_with("Constructor") && !SUPPORTING_TYPES.contains(&name)
452}
453
454pub fn builtins() -> Builtins {
456 let defs = builtin_package_declaration();
457 let mut types: Vec<String> = defs
459 .types
460 .keys()
461 .filter(|n| is_headline_type(n))
462 .cloned()
463 .collect();
464 types.push("Record".into());
465 types.sort();
466 let mut namespaces: Vec<String> = defs.namespaces.keys().cloned().collect();
467 namespaces.push(JSON_BUILTIN.to_string());
468 namespaces.sort();
469 Builtins { types, namespaces }
470}
471
472pub fn builtin_docs(name: &str) -> Option<String> {
476 match builtin_lookup(name) {
477 BuiltinLookup::Found(declarations) => Some(declarations),
478 BuiltinLookup::UnknownMember { .. } | BuiltinLookup::Unknown => None,
479 }
480}
481
482#[derive(Debug, Clone, PartialEq, Eq)]
486pub enum BuiltinLookup {
487 Found(String),
489 UnknownMember {
491 path: String,
492 member: String,
493 members: Vec<String>,
494 },
495 Unknown,
497}
498
499pub fn builtin_lookup(name: &str) -> BuiltinLookup {
508 if name.contains('#') {
511 return BuiltinLookup::Unknown;
512 }
513 let segments: Vec<&str> = name.split('.').filter(|s| !s.is_empty()).collect();
516 let Some((head, rest)) = segments.split_first() else {
517 return BuiltinLookup::Unknown;
518 };
519
520 if head.eq_ignore_ascii_case("Record") && rest.is_empty() {
521 return BuiltinLookup::Found("/** Record<K, V> accepts string, number, enum, literal-union, and template-pattern keys. With K = string, reads return V | undefined and writes require V. Finite literal keys are all required. Equivalent open syntax: { [key: string]: V }. */\ntype Record<K extends string | number, V> = { [P in K]: V };\n".into());
522 }
523 if head.eq_ignore_ascii_case(JSON_BUILTIN) {
524 let defs = json_package_declaration();
525 return walk_namespace(JSON_BUILTIN, &defs.namespaces[JSON_BUILTIN], rest);
526 }
527
528 let defs = builtin_package_declaration();
529 if let Some(canonical) = resolve_ignore_case(defs.namespaces.keys(), head) {
530 return walk_namespace(&canonical, &defs.namespaces[&canonical], rest);
531 }
532
533 let Some(canonical) = resolve_type_key(&defs.types, head) else {
534 return BuiltinLookup::Unknown;
535 };
536 match rest {
537 [] => {
538 let mut out = String::new();
539 render_type_with_ctor(&mut out, &defs.types, &canonical, "");
540 BuiltinLookup::Found(out.trim_end().to_string())
541 }
542 [member, deeper @ ..] => resolve_type_member(&defs.types, &canonical, member, deeper),
543 }
544}
545
546fn walk_namespace<'a>(head: &str, head_ns: &'a NamespaceSymbol, rest: &[&str]) -> BuiltinLookup {
548 let mut chain: Vec<String> = vec![head.to_string()];
549 let mut ns: &'a NamespaceSymbol = head_ns;
550
551 for (idx, seg) in rest.iter().enumerate() {
552 let tail = &rest[idx + 1..];
553
554 if let Some(canonical) = resolve_ignore_case(ns.namespaces.keys(), seg) {
555 let sub = &ns.namespaces[&canonical];
556 if tail.is_empty() {
557 let mut body = String::new();
558 render_namespace(&mut body, &canonical, sub, "");
559 return found_in(&chain, &body);
560 }
561 chain.push(canonical);
562 ns = sub;
563 continue;
564 }
565
566 if let Some(canonical) = resolve_type_key(&ns.types, seg) {
567 if tail.is_empty() {
568 let mut body = String::new();
569 render_namespace_member(&mut body, ns, &canonical);
570 return found_in(&chain, &body);
571 }
572 return match resolve_type_member(&ns.types, &canonical, tail[0], &tail[1..]) {
575 BuiltinLookup::Found(body) => found_in(&chain, &body),
576 miss => qualify(&chain, miss),
577 };
578 }
579
580 if let Some(canonical) = resolve_ignore_case(ns.values.keys(), seg) {
581 if tail.is_empty() {
582 let sym = &ns.values[&canonical];
583 let mut body = String::new();
584 push_doc(&mut body, value_doc(&sym.kind), "");
585 render_value(&mut body, &canonical, &sym.kind, "");
586 return found_in(&chain, &body);
587 }
588 chain.push(canonical);
590 return BuiltinLookup::UnknownMember {
591 path: chain.join("."),
592 member: tail.join("."),
593 members: Vec::new(),
594 };
595 }
596
597 return BuiltinLookup::UnknownMember {
598 path: chain.join("."),
599 member: (*seg).to_string(),
600 members: namespace_member_names(ns),
601 };
602 }
603
604 let mut out = String::new();
605 render_namespace(&mut out, head, head_ns, "");
606 BuiltinLookup::Found(out.trim_end().to_string())
607}
608
609fn resolve_type_member(
612 types: &BTreeMap<String, TypeSymbol>,
613 type_name: &str,
614 member: &str,
615 deeper: &[&str],
616) -> BuiltinLookup {
617 if !deeper.is_empty() {
618 return BuiltinLookup::UnknownMember {
619 path: format!("{type_name}.{member}"),
620 member: deeper.join("."),
621 members: Vec::new(),
622 };
623 }
624 let ctor = format!("{type_name}Constructor");
625 for owner in [type_name, ctor.as_str()] {
626 let Some(sym) = types.get(owner) else {
627 continue;
628 };
629 if let Some(body) = render_type_member(owner, &sym.kind, member) {
630 return BuiltinLookup::Found(body.trim_end().to_string());
631 }
632 }
633 BuiltinLookup::UnknownMember {
634 path: type_name.to_string(),
635 member: member.to_string(),
636 members: type_member_names(types, type_name),
637 }
638}
639
640fn render_namespace_member(out: &mut String, ns: &NamespaceSymbol, canonical: &str) {
648 if let Some(sym) = ns.values.get(canonical) {
649 push_doc(out, value_doc(&sym.kind), "");
650 render_value(out, canonical, &sym.kind, "");
651 }
652 if let Some(sym) = ns.types.get(canonical) {
653 push_doc(out, type_doc(&sym.kind), "");
654 render_type(out, canonical, sym, "");
655 }
656 let ctor = format!("{canonical}Constructor");
657 if let Some(sym) = ns.types.get(&ctor) {
658 push_doc(out, type_doc(&sym.kind), "");
659 render_type(out, &ctor, sym, "");
660 }
661}
662
663fn render_type_member(owner: &str, kind: &TypeKind, member: &str) -> Option<String> {
666 if let TypeKind::Class { .. } = kind {
667 return render_class_member(owner, kind, member);
668 }
669 let TypeKind::Interface {
670 generics,
671 methods,
672 properties,
673 ..
674 } = kind
675 else {
676 return None;
677 };
678 let mut body = String::new();
679 if let Some(name) = resolve_ignore_case(properties.keys(), member) {
680 let prop = &properties[&name];
681 push_doc(&mut body, &prop.doc, " ");
682 let ro = if prop.readonly { "readonly " } else { "" };
683 let opt = if prop.optional { "?" } else { "" };
684 let name = member_name(&name);
685 let _ = writeln!(body, " {ro}{name}{opt}: {};", prop.ty);
686 } else {
687 let name = resolve_ignore_case(methods.keys(), member)?;
688 let m = &methods[&name];
689 push_doc(&mut body, &m.doc, " ");
690 let optional = if m.optional { "?" } else { "" };
691 let name = method_prefix(&name);
692 let _ = writeln!(
693 body,
694 " {name}{optional}{}({}): {};",
695 generics_str(&m.generics),
696 params_str(&m.params, m.doc.as_ref()),
697 m.ret
698 );
699 }
700 Some(format!(
701 "interface {owner}{} {{\n{body}}}\n",
702 generics_str(generics)
703 ))
704}
705
706fn render_class_member(owner: &str, kind: &TypeKind, member: &str) -> Option<String> {
710 let TypeKind::Class {
711 generics,
712 fields,
713 methods,
714 method_visibility,
715 statics,
716 static_visibility,
717 static_fields,
718 ..
719 } = kind
720 else {
721 return None;
722 };
723 let mut body = String::new();
724 if let Some(name) = resolve_ignore_case(static_fields.keys(), member)
725 .filter(|n| static_fields[n].visibility != crate::Visibility::Private)
726 {
727 let field = &static_fields[&name];
728 push_doc(&mut body, &field.doc, " ");
729 let ro = if field.readonly { "readonly " } else { "" };
730 let name = member_name(&name);
731 let _ = writeln!(body, " static {ro}{name}: {};", field.ty);
732 } else if let Some(name) = resolve_ignore_case(statics.keys(), member)
733 .filter(|n| static_visibility.get(n) != Some(&crate::Visibility::Private))
734 {
735 let m = &statics[&name];
736 push_doc(&mut body, &m.doc, " ");
737 let name = member_name(&name);
738 let _ = writeln!(
739 body,
740 " static {name}{}({}): {};",
741 generics_str(&m.generics),
742 params_str(&m.params, m.doc.as_ref()),
743 m.ret
744 );
745 } else if let Some(name) = resolve_ignore_case(fields.keys(), member)
746 .filter(|n| fields[n].visibility != crate::Visibility::Private)
747 {
748 let field = &fields[&name];
749 push_doc(&mut body, &field.doc, " ");
750 let ro = if field.readonly { "readonly " } else { "" };
751 let opt = if field.optional { "?" } else { "" };
752 let name = member_name(&name);
753 let _ = writeln!(body, " {ro}{name}{opt}: {};", field.ty);
754 } else {
755 let name = resolve_ignore_case(methods.keys(), member)
756 .filter(|n| method_visibility.get(n) != Some(&crate::Visibility::Private))?;
757 let m = &methods[&name];
758 push_doc(&mut body, &m.doc, " ");
759 let optional = if m.optional { "?" } else { "" };
760 let name = member_name(&name);
761 let _ = writeln!(
762 body,
763 " {name}{optional}{}({}): {};",
764 generics_str(&m.generics),
765 params_str(&m.params, m.doc.as_ref()),
766 m.ret
767 );
768 }
769 Some(format!(
770 "class {owner}{} {{\n{body}}}\n",
771 generics_str(generics)
772 ))
773}
774
775fn type_member_names(types: &BTreeMap<String, TypeSymbol>, type_name: &str) -> Vec<String> {
777 let ctor = format!("{type_name}Constructor");
778 let mut names: Vec<String> =
779 [type_name, ctor.as_str()]
780 .iter()
781 .filter_map(|owner| types.get(*owner))
782 .flat_map(|sym| match &sym.kind {
783 TypeKind::Interface {
784 methods,
785 properties,
786 ..
787 } => properties
788 .keys()
789 .chain(methods.keys())
790 .cloned()
791 .collect::<Vec<_>>(),
792 TypeKind::Class {
795 fields,
796 methods,
797 method_visibility,
798 statics,
799 static_visibility,
800 static_fields,
801 ..
802 } => {
803 static_fields
804 .iter()
805 .filter(|(_, f)| f.visibility != crate::Visibility::Private)
806 .map(|(n, _)| n)
807 .chain(statics.keys().filter(|n| {
808 static_visibility.get(*n) != Some(&crate::Visibility::Private)
809 }))
810 .chain(
811 fields
812 .iter()
813 .filter(|(_, f)| f.visibility != crate::Visibility::Private)
814 .map(|(n, _)| n),
815 )
816 .chain(methods.keys().filter(|n| {
817 method_visibility.get(*n) != Some(&crate::Visibility::Private)
818 }))
819 .cloned()
820 .collect::<Vec<_>>()
821 }
822 _ => Vec::new(),
823 })
824 .collect();
825 names.sort();
826 names.dedup();
827 names
828}
829
830fn namespace_member_names(ns: &NamespaceSymbol) -> Vec<String> {
833 let mut names: Vec<String> = ns
834 .values
835 .keys()
836 .chain(ns.types.keys())
837 .chain(ns.namespaces.keys())
838 .filter(|n| !n.ends_with("Constructor"))
839 .cloned()
840 .collect();
841 names.sort();
842 names.dedup();
843 names
844}
845
846fn resolve_type_key(types: &BTreeMap<String, TypeSymbol>, name: &str) -> Option<String> {
850 resolve_ignore_case(types.keys(), name).or_else(|| {
851 resolve_ignore_case(types.keys(), &format!("{name}Constructor"))
852 .map(|c| c.trim_end_matches("Constructor").to_string())
853 })
854}
855
856fn found_in(chain: &[String], body: &str) -> BuiltinLookup {
858 let mut out = body.trim_end().to_string();
859 for name in chain.iter().rev() {
860 out = format!("namespace {name} {{\n{}\n}}", indent_block(&out, " "));
861 }
862 BuiltinLookup::Found(out)
863}
864
865fn qualify(chain: &[String], miss: BuiltinLookup) -> BuiltinLookup {
867 match miss {
868 BuiltinLookup::UnknownMember {
869 path,
870 member,
871 members,
872 } if !chain.is_empty() => BuiltinLookup::UnknownMember {
873 path: format!("{}.{path}", chain.join(".")),
874 member,
875 members,
876 },
877 other => other,
878 }
879}
880
881pub enum Resolution {
887 Module(ModuleDoc),
889 Builtin { name: String, declarations: String },
891 UnknownMember {
893 path: String,
894 member: String,
895 members: Vec<String>,
896 },
897 Unknown,
899}
900
901pub fn resolve(name: &str) -> Resolution {
904 Stdlib::core().resolve(name)
905}
906
907const OMITTED_GLOBALS: &[(&str, &str)] = &[("Date", "Temporal")];
919
920pub fn suggest(name: &str, extra: &[String]) -> Option<String> {
921 suggest_filtered(name, extra, |_| true)
922}
923
924pub fn suggest_filtered(
926 name: &str,
927 extra: &[String],
928 visible: impl Fn(&str) -> bool,
929) -> Option<String> {
930 Stdlib::core().suggest_filtered(name, extra, visible)
931}
932
933fn name_tail(name: &str) -> &str {
936 let after_scheme = name.rsplit_once(':').map_or(name, |(_, t)| t);
937 after_scheme
938 .rsplit_once('/')
939 .map_or(after_scheme, |(_, t)| t)
940}
941
942#[derive(Debug, Clone, PartialEq, Eq)]
946pub struct CatalogEntry {
947 pub name: String,
948 pub source: String,
949 pub description: String,
950}
951
952pub struct Catalog {
954 pub entries: Vec<CatalogEntry>,
955 pub remaining: usize,
958}
959
960pub const CATALOG_LIMIT: usize = 50;
962
963pub fn builtins_pointer(fetch_with: &str) -> String {
971 let head = "Language built-ins (Array, Map, String, Temporal, JSON, …)";
972 format!("{head} are always in scope without an import — fetch them with {fetch_with}.")
973}
974
975pub fn package_correcting_message(name: &str, fetch_with: &str) -> String {
984 format!(
985 "`{name}` is a package, not a language built-in. {fetch_with} for its declarations, \
986 and write `import ... from \"{name}\"` to use it."
987 )
988}
989
990pub const SOURCE_STDLIB: &str = "stdlib";
992
993pub fn catalog(extra: Vec<CatalogEntry>) -> Catalog {
995 catalog_filtered(extra, |_| true)
996}
997
998pub fn catalog_filtered(extra: Vec<CatalogEntry>, visible: impl Fn(&str) -> bool) -> Catalog {
1000 Stdlib::core().catalog_filtered(extra, visible)
1001}
1002
1003pub fn unknown_member_message(path: &str, member: &str, members: &[String]) -> String {
1005 if members.is_empty() {
1006 return format!("`{path}` has no member `{member}` — `{path}` has no members.");
1007 }
1008 format!(
1009 "`{path}` has no member `{member}`. Available members of `{path}`: {}.",
1010 members.join(", ")
1011 )
1012}
1013
1014pub fn builtin_no_import_note(name: &str) -> String {
1018 format!("`{name}` is a language built-in, always in scope — do not write an `import` for it.")
1019}
1020
1021pub fn bare_name_note(requested: &str, canonical: &str) -> String {
1026 format!(
1027 "Wrong package name: `{requested}` does not exist. The package is `{canonical}`. \
1028 Import `{canonical}` — write `import ... from \"{canonical}\"`. Do not import `{requested}`."
1029 )
1030}
1031
1032pub fn ambiguous_bare_name_message(requested: &str, names: &[String]) -> String {
1035 let listed = names
1036 .iter()
1037 .map(|name| format!("`{name}`"))
1038 .collect::<Vec<_>>()
1039 .join(", ");
1040 format!(
1041 "Wrong package name: `{requested}` does not exist. More than one package has that short name. \
1042 Import one of these exact names, not `{requested}`: {listed}."
1043 )
1044}
1045
1046fn indent_block(body: &str, pad: &str) -> String {
1047 body.lines()
1048 .map(|l| {
1049 if l.is_empty() {
1050 String::new()
1051 } else {
1052 format!("{pad}{l}")
1053 }
1054 })
1055 .collect::<Vec<_>>()
1056 .join("\n")
1057}
1058
1059fn builtin_package_declaration() -> PackageDeclaration {
1060 let mut defs = prelude_package_declaration();
1061 let prelude_decl = crate::runtime::prelude::package_declaration();
1062 for (name, namespace) in prelude_decl.namespaces {
1063 defs.namespaces.entry(name).or_insert(namespace);
1064 }
1065 defs
1066}
1067
1068fn resolve_ignore_case<'a>(keys: impl Iterator<Item = &'a String>, name: &str) -> Option<String> {
1071 let mut fallback = None;
1072 for key in keys {
1073 if key == name {
1074 return Some(key.clone());
1075 }
1076 if fallback.is_none() && key.eq_ignore_ascii_case(name) {
1077 fallback = Some(key.clone());
1078 }
1079 }
1080 fallback
1081}
1082
1083fn user_modules(stdlib: Stdlib) -> Vec<PackageDeclaration> {
1085 stdlib
1086 .package_declarations()
1087 .into_iter()
1088 .filter(|d| d.package_name != INTERNAL_MODULE)
1089 .collect()
1090}
1091
1092fn module_description(name: &str) -> &'static str {
1095 match name {
1096 "submilli:agents" => {
1097 "Harness sub-agents: runText(agent, input) and run<T>(agent, input), and list() to discover them."
1098 }
1099 "submilli:crypto" => "Hashing, HMAC, and random bytes.",
1100 "submilli:code" => {
1101 "Workspace tools: numbered reads, search, glob, tree, anchored edits and unified diffs."
1102 }
1103 "submilli:embedding" => {
1104 "Remote text embeddings: embed batches into sealed vectors, and models() to discover aliases."
1105 }
1106 "submilli:fs" => "Sandbox filesystem: files and directories under the VFS root.",
1107 "submilli:git" => "Capability-controlled Git repositories in the VFS, with HTTPS remotes.",
1108 "submilli:http" => "Outbound HTTP requests, and downloads into the filesystem.",
1109 "submilli:llm" => {
1110 "Gated model calls: text/texts for text, call<T>/batch<T> for checked values, and models() to discover them."
1111 }
1112 "submilli:secrets" => "Policy-gated access to Blueprint-declared secrets.",
1113 "submilli:session" => {
1114 "Session-scoped key-value state and durable package-owned resource cleanup."
1115 }
1116 "submilli:skills" => "Harness skills: list(), load(name), and skill.readFile(path).",
1117 "submilli:url" => "URL parse/build and query-string handling. Pure compute.",
1118 "submilli:variables" => "Immutable Blueprint/session configuration for packages.",
1119 "submilli:uuid" => "UUID v4/v7 generation and validation.",
1120 _ => "",
1121 }
1122}
1123
1124fn matches_query(defs: &PackageDeclaration, q: &str) -> bool {
1125 if q.is_empty() {
1126 return true;
1127 }
1128 let in_symbols = defs
1129 .values
1130 .keys()
1131 .chain(defs.types.keys())
1132 .any(|k| k.to_lowercase().contains(q));
1133 defs.package_name.to_lowercase().contains(q)
1134 || module_description(&defs.package_name)
1135 .to_lowercase()
1136 .contains(q)
1137 || in_symbols
1138}
1139
1140pub fn render_declarations(defs: &PackageDeclaration) -> String {
1144 render_declarations_for(defs, DocAudience::Author)
1145}
1146
1147pub fn render_declarations_for(defs: &PackageDeclaration, audience: DocAudience) -> String {
1150 let defs = with_public_type_names(defs);
1151 let defs = defs.as_ref();
1152 let mut out = String::new();
1153 for (name, sym) in &defs.values {
1154 push_doc_for(&mut out, value_doc(&sym.kind), "", audience);
1155 render_value(&mut out, name, &sym.kind, "");
1156 }
1157 for (name, sym) in &defs.types {
1158 push_doc_for(&mut out, type_doc(&sym.kind), "", audience);
1159 render_type_for(&mut out, name, sym, "", audience);
1160 }
1161 out.trim_end().to_string()
1162}
1163
1164pub fn render_selected_declarations(defs: &PackageDeclaration) -> String {
1166 enum Part<'a> {
1167 Namespace(&'a str, &'a NamespaceSymbol, usize),
1168 Close(usize),
1169 }
1170 let defs = with_public_type_names(defs);
1171 let mut top = defs.as_ref().clone();
1172 top.types.retain(|name, _| !name.contains('.'));
1174 let mut out = render_declarations_for(&top, DocAudience::Agent);
1175 let mut pending: Vec<_> = defs
1176 .namespaces
1177 .iter()
1178 .rev()
1179 .map(|(name, ns)| Part::Namespace(name, ns, 0))
1180 .collect();
1181 while let Some(part) = pending.pop() {
1182 match part {
1183 Part::Close(depth) => {
1184 let _ = writeln!(out, "{}}}", " ".repeat(depth));
1185 }
1186 Part::Namespace(name, ns, depth) => {
1187 if ns.values.is_empty() && ns.types.is_empty() && ns.namespaces.is_empty() {
1188 continue;
1189 }
1190 let indent = " ".repeat(depth);
1191 let _ = writeln!(out, "\n{indent}namespace {name} {{");
1192 let inner = format!("{indent} ");
1193 for (name, value) in &ns.values {
1194 push_doc_for(&mut out, value_doc(&value.kind), &inner, DocAudience::Agent);
1195 render_value(&mut out, name, &value.kind, &inner);
1196 }
1197 for (name, symbol) in &ns.types {
1198 push_doc_for(&mut out, type_doc(&symbol.kind), &inner, DocAudience::Agent);
1199 render_type_for(&mut out, name, symbol, &inner, DocAudience::Agent);
1200 }
1201 pending.push(Part::Close(depth));
1202 pending.extend(
1203 ns.namespaces
1204 .iter()
1205 .rev()
1206 .map(|(name, ns)| Part::Namespace(name, ns, depth + 1)),
1207 );
1208 }
1209 }
1210 }
1211 out.trim().to_owned()
1212}
1213
1214fn with_public_type_names(defs: &PackageDeclaration) -> Cow<'_, PackageDeclaration> {
1223 let mut public: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
1226 for (name, symbol) in &defs.types {
1227 if symbol.mangled_name != crate::mangle::package_symbol(&defs.package_name, name) {
1228 public
1229 .entry(symbol.mangled_name.as_str())
1230 .or_default()
1231 .push(name.as_str());
1232 }
1233 }
1234 if public.is_empty() {
1235 return Cow::Borrowed(defs);
1236 }
1237 let Ok(mut value) = serde_json::to_value(defs) else {
1238 return Cow::Borrowed(defs);
1239 };
1240 let mut renamed = false;
1241 let mut pending = vec![&mut value];
1242 while let Some(node) = pending.pop() {
1243 match node {
1244 serde_json::Value::Object(fields) => {
1245 let Some(serde_json::Value::String(mangled)) = fields.get("mangled") else {
1246 pending.extend(fields.values_mut());
1247 continue;
1248 };
1249 if let Some(names) = public.get(mangled.as_str())
1252 && let Some(serde_json::Value::String(name)) = fields.get_mut("name")
1253 && !names.contains(&name.as_str())
1254 && let Some(first) = names.first()
1255 {
1256 *name = first.to_string();
1257 renamed = true;
1258 }
1259 pending.extend(fields.get_mut("args"));
1260 }
1261 serde_json::Value::Array(items) => pending.extend(items),
1262 _ => {}
1263 }
1264 }
1265 if !renamed {
1266 return Cow::Borrowed(defs);
1267 }
1268 serde_json::from_value(value).map_or(Cow::Borrowed(defs), Cow::Owned)
1269}
1270
1271pub fn render_stdlib_d_ts() -> String {
1278 Stdlib::core().render_stdlib_d_ts()
1279}
1280
1281pub fn render_packages_d_ts(
1291 packages: &[&PackageDeclaration],
1292 context: &[&PackageDeclaration],
1293) -> String {
1294 Stdlib::core().render_packages_d_ts(packages, context)
1295}
1296
1297fn global_classes() -> BTreeMap<String, String> {
1300 let defs = builtin_package_declaration();
1301 defs.types
1302 .into_iter()
1303 .filter(|(name, symbol)| {
1304 matches!(symbol.kind, TypeKind::Class { .. })
1305 && !is_hidden_prelude_type(&defs.package_name, name)
1306 })
1307 .map(|(name, symbol)| (symbol.mangled_name.as_str().to_string(), name))
1308 .collect()
1309}
1310
1311fn editor_stdlib_modules(stdlib: Stdlib) -> Vec<PackageDeclaration> {
1312 let mut modules = user_modules(stdlib);
1313 modules.push(crate::stdlib::test::package_declaration());
1314 modules.push(crate::stdlib::security::package_declaration());
1315 modules.sort_by(|a, b| a.package_name.cmp(&b.package_name));
1316 modules
1317}
1318
1319struct TypeExporters<'a> {
1321 by_mangled: BTreeMap<&'a str, (ExportedType<'a>, &'a str)>,
1323 by_module: BTreeMap<&'a str, BTreeMap<&'a str, ExportedType<'a>>>,
1326}
1327
1328#[derive(Clone, Copy)]
1329struct ExportedType<'a> {
1330 name: &'a str,
1331 is_value: bool,
1334}
1335
1336impl<'a> TypeExporters<'a> {
1337 fn new(modules: impl Iterator<Item = &'a PackageDeclaration>) -> Self {
1338 let mut by_mangled = BTreeMap::new();
1339 let mut by_module: BTreeMap<&str, BTreeMap<&str, ExportedType>> = BTreeMap::new();
1340 for defs in modules {
1341 let module = defs.package_name.as_str();
1342 for (name, symbol) in &defs.types {
1343 let exported = ExportedType {
1344 name: name.as_str(),
1345 is_value: matches!(
1346 symbol.kind,
1347 TypeKind::Class { .. }
1348 | TypeKind::NumberEnum { .. }
1349 | TypeKind::StringEnum { .. }
1350 ),
1351 };
1352 by_mangled.insert(symbol.mangled_name.as_str(), (exported, module));
1353 by_module
1354 .entry(module)
1355 .or_default()
1356 .insert(name.as_str(), exported);
1357 }
1358 }
1359 Self {
1360 by_mangled,
1361 by_module,
1362 }
1363 }
1364
1365 fn resolve(&self, reference: &TypeReference) -> Option<(ExportedType<'a>, &'a str)> {
1368 if let Some(&exported) = self.by_mangled.get(reference.mangled.as_str()) {
1369 return Some(exported);
1370 }
1371 let (module, types) = self.by_module.get_key_value(reference.package.as_str())?;
1372 Some((*types.get(reference.name.as_str())?, module))
1373 }
1374}
1375
1376struct TypeReference {
1380 mangled: String,
1381 package: String,
1382 name: String,
1383}
1384
1385struct BlockImports<'a> {
1387 types: BTreeMap<String, (&'a str, &'a str)>,
1390 parents: BTreeMap<&'a str, String>,
1393}
1394
1395fn block_imports<'a>(
1408 defs: &'a PackageDeclaration,
1409 exporters: &TypeExporters<'a>,
1410) -> BlockImports<'a> {
1411 let mut types = BTreeMap::new();
1412 for reference in rendered_type_references(defs) {
1413 let Some((exported, module)) = exporters.resolve(&reference) else {
1414 continue;
1415 };
1416 if module != defs.package_name && !declares(defs, &reference.name, exported.is_value) {
1417 types
1418 .entry(reference.name)
1419 .or_insert((exported.name, module));
1420 }
1421 }
1422 let mut parents = BTreeMap::new();
1423 for (mangled, exported, module) in class_parents(defs, exporters) {
1424 if module == defs.package_name {
1425 parents.insert(mangled, exported.name.to_string());
1426 continue;
1427 }
1428 let already_imported = types
1429 .iter()
1430 .find(|(_, target)| **target == (exported.name, module))
1431 .map(|(local, _)| local.clone());
1432 let local = if let Some(local) = already_imported {
1433 local
1434 } else {
1435 let local = free_local_name(defs, &types, exported.name);
1436 types.insert(local.clone(), (exported.name, module));
1437 local
1438 };
1439 parents.insert(mangled, local);
1440 }
1441 BlockImports { types, parents }
1442}
1443
1444fn free_local_name(
1448 defs: &PackageDeclaration,
1449 imports: &BTreeMap<String, (&str, &str)>,
1450 public: &str,
1451) -> String {
1452 let is_free = |name: &str| !imports.contains_key(name) && !declares(defs, name, true);
1453 if is_free(public) {
1454 return public.to_string();
1455 }
1456 let taken = imports
1457 .len()
1458 .saturating_add(defs.types.len())
1459 .saturating_add(defs.values.len())
1460 .saturating_add(defs.namespaces.len());
1461 (1..=taken.saturating_add(1))
1462 .map(|suffix| format!("{public}{suffix}"))
1463 .find(|name| is_free(name))
1464 .unwrap_or_else(|| format!("{public}{}", taken.saturating_add(1)))
1465}
1466
1467fn declares(defs: &PackageDeclaration, name: &str, borrowed_is_value: bool) -> bool {
1470 defs.types.contains_key(name)
1471 || (borrowed_is_value
1472 && (defs.values.contains_key(name) || defs.namespaces.contains_key(name)))
1473}
1474
1475fn class_parents<'a>(
1478 defs: &'a PackageDeclaration,
1479 exporters: &TypeExporters<'a>,
1480) -> Vec<(&'a str, ExportedType<'a>, &'a str)> {
1481 defs.types
1482 .values()
1483 .filter_map(|symbol| match &symbol.kind {
1484 TypeKind::Class {
1485 extends: Some(extends),
1486 ..
1487 } => {
1488 let parent = extends.parent.as_str();
1489 let &(exported, module) = exporters.by_mangled.get(parent)?;
1490 Some((parent, exported, module))
1491 }
1492 _ => None,
1493 })
1494 .collect()
1495}
1496
1497fn rendered_type_references(defs: &PackageDeclaration) -> Vec<TypeReference> {
1509 let types: BTreeMap<&String, TypeSymbol> = defs
1510 .types
1511 .iter()
1512 .map(|(name, symbol)| (name, public_surface(symbol)))
1513 .collect();
1514 collect_type_references(&[
1515 serde_json::to_value(&defs.values),
1516 serde_json::to_value(&types),
1517 serde_json::to_value(&defs.namespaces),
1518 ])
1519}
1520
1521pub(crate) fn type_references(
1524 rendered: &[Result<serde_json::Value, serde_json::Error>],
1525) -> Vec<String> {
1526 collect_type_references(rendered)
1527 .into_iter()
1528 .map(|reference| reference.mangled)
1529 .collect()
1530}
1531
1532pub(crate) fn type_symbol_references(symbol: &TypeSymbol) -> Vec<String> {
1536 let mut references = type_references(&[serde_json::to_value(public_surface(symbol))]);
1537 if let TypeKind::Class {
1538 extends,
1539 implements,
1540 ..
1541 } = &symbol.kind
1542 {
1543 references.extend(
1544 extends
1545 .iter()
1546 .map(|extends| extends.parent.as_str().to_string()),
1547 );
1548 references.extend(implements.iter().map(|name| name.as_str().to_string()));
1549 }
1550 references
1551}
1552
1553fn collect_type_references(
1554 rendered: &[Result<serde_json::Value, serde_json::Error>],
1555) -> Vec<TypeReference> {
1556 let mut pending: Vec<&serde_json::Value> = rendered.iter().flatten().collect();
1558 let mut references = Vec::new();
1559 while let Some(value) = pending.pop() {
1560 match value {
1561 serde_json::Value::Object(fields) => {
1562 let Some(serde_json::Value::String(mangled)) = fields.get("mangled") else {
1563 pending.extend(fields.values());
1564 continue;
1565 };
1566 let text = |key: &str| {
1567 fields
1568 .get(key)
1569 .and_then(serde_json::Value::as_str)
1570 .unwrap_or_default()
1571 .to_string()
1572 };
1573 references.push(TypeReference {
1574 mangled: mangled.clone(),
1575 package: text("package"),
1576 name: text("name"),
1577 });
1578 pending.extend(fields.get("args"));
1579 }
1580 serde_json::Value::Array(items) => pending.extend(items),
1581 _ => {}
1582 }
1583 }
1584 references
1585}
1586
1587fn public_surface(symbol: &TypeSymbol) -> TypeSymbol {
1590 let mut symbol = symbol.clone();
1591 if let TypeKind::Class {
1592 fields,
1593 narrowing_checks,
1594 methods,
1595 method_visibility,
1596 accessors,
1597 constructor,
1598 constructor_visibility,
1599 statics,
1600 static_visibility,
1601 static_fields,
1602 ..
1603 } = &mut symbol.kind
1604 {
1605 if *constructor_visibility == crate::Visibility::Private {
1606 constructor.clear();
1607 }
1608 let is_public = |visibility: Option<&crate::Visibility>| {
1609 visibility != Some(&crate::Visibility::Private)
1610 };
1611 accessors.retain(|accessor| is_public(fields.get(accessor.name()).map(|f| &f.visibility)));
1612 fields.retain(|_, field| is_public(Some(&field.visibility)));
1613 static_fields.retain(|_, field| is_public(Some(&field.visibility)));
1614 methods.retain(|name, _| is_public(method_visibility.get(name)));
1615 statics.retain(|name, _| is_public(static_visibility.get(name)));
1616 narrowing_checks.clear();
1617 }
1618 symbol
1619}
1620
1621pub fn render_lib_submilli_d_ts() -> String {
1624 let defs = builtin_package_declaration();
1625 let globals = global_classes();
1626 let parent = |extends: &ClassExtends| {
1627 let global = globals.get(extends.parent.as_str())?;
1628 Some(ts_named_type(global, &extends.args))
1629 };
1630 let mut out = String::new();
1631 render_ts_declarations(&mut out, &defs, "", "declare ", &parent);
1632 render_ts_compiler_globals(&mut out, &defs);
1633 render_ts_declarations(&mut out, &global_functions(), "", "declare ", &|_| None);
1634 let json = json_package_declaration();
1635 render_ts_declarations(&mut out, &json, "", "declare ", &|_| None);
1636 out.trim_end().to_string()
1637}
1638
1639fn global_functions() -> PackageDeclaration {
1643 const NAMES: [&str; 6] = [
1644 "parseInt",
1645 "parseFloat",
1646 "encodeURIComponent",
1647 "encodeURI",
1648 "decodeURIComponent",
1649 "decodeURI",
1650 ];
1651 let mut globals = PackageDeclaration::with_package(crate::mangle::PRELUDE_PACKAGE);
1652 let sources = crate::runtime::host_package_declarations()
1653 .into_iter()
1654 .chain([crate::runtime::prelude::package_declaration()]);
1655 for source in sources {
1656 for symbol in source.values.into_values() {
1657 if NAMES.contains(&symbol.name.as_str()) {
1658 globals.values.insert(symbol.name.clone(), symbol);
1659 }
1660 }
1661 }
1662 globals
1663}
1664
1665fn render_ts_compiler_globals(out: &mut String, defs: &PackageDeclaration) {
1666 if !defs.types.contains_key("Function") {
1667 out.push_str(
1668 "interface Function {\n /** The parameters before the first optional, defaulted or rest one. */\n readonly length: number;\n}\n\n",
1669 );
1670 }
1671 out.push_str("interface CallableFunction extends Function {}\n\n");
1672 out.push_str("interface NewableFunction extends Function {}\n\n");
1673 out.push_str("interface IArguments {}\n\n");
1674 out.push_str(
1675 "/** Throw `Error(message)` if `condition` is false. Compiler intrinsic — always in scope, not imported. */\n\
1676 declare function assert(condition: boolean, message?: string): void;\n\n",
1677 );
1678 render_ts_checker_plumbing(out, defs);
1679}
1680
1681fn render_ts_checker_plumbing(out: &mut String, defs: &PackageDeclaration) {
1689 out.push_str("declare const Symbol: { readonly iterator: unique symbol };\n\n");
1690 out.push_str(
1691 "interface Array<T> {\n [index: number]: T;\n [Symbol.iterator](): Iterator<T>;\n}\n\n",
1692 );
1693 for (name, sym) in &defs.types {
1694 if let TypeKind::Interface {
1695 generics, methods, ..
1696 } = &sym.kind
1697 && let Some(m) = methods.get("iterator")
1698 {
1699 let _ = writeln!(
1700 out,
1701 "interface {name}{} {{\n [Symbol.iterator](): {};\n}}\n",
1702 ts_interface_generics(name, generics, "declare "),
1703 ts_type(&m.ret)
1704 );
1705 }
1706 }
1707}
1708
1709fn render_declare_module(out: &mut String, defs: &PackageDeclaration) {
1710 let _ = writeln!(out, "declare module \"{}\" {{", defs.package_name);
1711 render_ts_declarations(out, defs, " ", "export ", &|_| None);
1712 let _ = writeln!(out, "}}\n");
1713}
1714
1715fn render_ts_declarations(
1718 out: &mut String,
1719 defs: &PackageDeclaration,
1720 indent: &str,
1721 export_prefix: &str,
1722 class_parent: &dyn Fn(&ClassExtends) -> Option<String>,
1723) {
1724 for (name, sym) in &defs.values {
1725 if is_hidden_prelude_value(&defs.package_name, name) {
1726 continue;
1727 }
1728 push_doc(out, value_doc(&sym.kind), indent);
1729 render_ts_value(out, name, &sym.kind, indent, export_prefix);
1730 }
1731 let pairs_constructors = defs.package_name.starts_with("submilli:");
1734 for (name, sym) in &defs.types {
1735 if is_hidden_prelude_type(&defs.package_name, name) {
1736 continue;
1737 }
1738 if pairs_constructors && name.ends_with("Constructor") {
1739 continue;
1740 }
1741 if pairs_constructors && defs.types.contains_key(&format!("{name}Constructor")) {
1742 render_ts_type_with_ctor(
1743 out,
1744 &defs.types,
1745 name,
1746 indent,
1747 export_prefix,
1748 !defs.values.contains_key(name),
1749 );
1750 } else {
1751 push_doc(out, type_doc(&sym.kind), indent);
1752 let parent = match &sym.kind {
1753 TypeKind::Class {
1754 extends: Some(extends),
1755 ..
1756 } => class_parent(extends),
1757 _ => None,
1758 };
1759 render_ts_type(
1760 out,
1761 name,
1762 &sym.kind,
1763 indent,
1764 export_prefix,
1765 parent.as_deref().or_else(|| intrinsic_array_parent(sym)),
1766 );
1767 }
1768 }
1769 for (name, ns) in &defs.namespaces {
1770 render_ts_namespace(out, name, ns, indent, export_prefix);
1771 }
1772}
1773
1774fn is_hidden_prelude_value(package_name: &str, name: &str) -> bool {
1775 package_name == crate::mangle::PRELUDE_PACKAGE
1776 && (name.starts_with("string_")
1777 || matches!(
1778 name,
1779 "string_concat" | "string_eq" | "string_length" | "string_cmp"
1780 ))
1781}
1782
1783fn is_hidden_prelude_type(package_name: &str, name: &str) -> bool {
1784 package_name == crate::mangle::PRELUDE_PACKAGE && name.contains('#')
1785}
1786
1787fn render_ts_value(
1788 out: &mut String,
1789 name: &str,
1790 kind: &ValueKind,
1791 indent: &str,
1792 export_prefix: &str,
1793) {
1794 let reserved_export = export_prefix == "export " && is_ts_reserved_word(name);
1795 let declared_name = if reserved_export {
1796 format!("{name}_")
1797 } else {
1798 name.to_string()
1799 };
1800 let decl_export_prefix = if reserved_export { "" } else { export_prefix };
1801 match kind {
1802 ValueKind::Function {
1803 generics,
1804 params,
1805 ret,
1806 type_predicate,
1807 ..
1808 } => {
1809 let ret = ts_return_type(
1810 params,
1811 ret,
1812 type_predicate.as_ref(),
1813 value_doc(kind).as_ref(),
1814 );
1815 let _ = writeln!(
1816 out,
1817 "{indent}{decl_export_prefix}function {declared_name}{}({}): {ret};",
1818 generics_str(generics),
1819 ts_params_str(params, value_doc(kind).as_ref())
1820 );
1821 }
1822 ValueKind::Let { ty, .. } => {
1823 let _ = writeln!(
1824 out,
1825 "{indent}{decl_export_prefix}let {declared_name}: {};",
1826 ts_type(ty)
1827 );
1828 }
1829 ValueKind::Const { ty, .. } => {
1830 let _ = writeln!(
1831 out,
1832 "{indent}{decl_export_prefix}const {declared_name}: {};",
1833 ts_type(ty)
1834 );
1835 }
1836 }
1837 if reserved_export {
1838 let _ = writeln!(out, "{indent}export {{ {declared_name} as {name} }};");
1839 }
1840 out.push('\n');
1841}
1842
1843fn intrinsic_array_parent(sym: &TypeSymbol) -> Option<&'static str> {
1844 (sym.mangled_name == crate::mangle::prelude("RegExpMatchArray")
1845 || sym.mangled_name == crate::mangle::prelude("RegExpExecArray"))
1846 .then_some("Array<string | undefined>")
1847}
1848
1849fn utility_declaration(name: &str, body: &Type) -> Option<&'static str> {
1852 match body {
1853 Type::AliasRef {
1854 mangled,
1855 name: applied,
1856 ..
1857 } if applied == name => crate::runtime::prelude::utility_types::declaration_text(mangled),
1858 _ => None,
1859 }
1860}
1861
1862fn render_ts_type(
1864 out: &mut String,
1865 name: &str,
1866 kind: &TypeKind,
1867 indent: &str,
1868 export_prefix: &str,
1869 parent: Option<&str>,
1870) {
1871 let inner = format!("{indent} ");
1872 match kind {
1873 TypeKind::Interface {
1874 generics,
1875 methods,
1876 properties,
1877 index,
1878 ..
1879 } => {
1880 let _ = writeln!(
1881 out,
1882 "{indent}{export_prefix}interface {name}{}{} {{",
1883 ts_interface_generics(name, generics, export_prefix),
1884 parent
1885 .map(|parent| format!(" extends {parent}"))
1886 .unwrap_or_default()
1887 );
1888 if let Some(index) = index {
1889 let ro = if index.readonly { "readonly " } else { "" };
1890 let _ = writeln!(
1891 out,
1892 "{inner}{ro}[key: {}]: {};",
1893 ts_type(&index.key),
1894 ts_type(&index.value)
1895 );
1896 }
1897 for (pname, prop) in properties {
1898 push_doc(out, &prop.doc, &inner);
1899 let ro = if prop.readonly { "readonly " } else { "" };
1900 let opt = if prop.optional { "?" } else { "" };
1901 let pname = member_name(pname);
1902 let _ = writeln!(out, "{inner}{ro}{pname}{opt}: {};", ts_type(&prop.ty));
1903 }
1904 for (mname, m) in methods {
1905 push_doc(out, &m.doc, &inner);
1906 let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1907 let optional = if m.optional { "?" } else { "" };
1908 let mname = method_prefix(mname);
1909 let _ = writeln!(
1910 out,
1911 "{inner}{mname}{optional}{}({}): {ret};",
1912 generics_str(&m.generics),
1913 ts_params_str(&m.params, m.doc.as_ref())
1914 );
1915 }
1916 let _ = writeln!(out, "{indent}}}\n");
1917 }
1918 TypeKind::NumberEnum { variants, .. } => {
1919 let _ = writeln!(out, "{indent}{export_prefix}enum {name} {{");
1920 for (v, value) in variants {
1921 let _ = writeln!(out, "{inner}{v} = {value},");
1922 }
1923 let _ = writeln!(out, "{indent}}}\n");
1924 }
1925 TypeKind::StringEnum { variants, .. } => {
1926 let _ = writeln!(out, "{indent}{export_prefix}enum {name} {{");
1927 for (v, value) in variants {
1928 let _ = writeln!(out, "{inner}{v} = \"{}\",", escape_string_literal(value));
1929 }
1930 let _ = writeln!(out, "{indent}}}\n");
1931 }
1932 TypeKind::Alias { ty, .. } if let Some(text) = utility_declaration(name, ty) => {
1933 let _ = writeln!(out, "{indent}{export_prefix}{text}\n");
1934 }
1935 TypeKind::Alias { generics, ty, .. } => {
1936 let _ = writeln!(
1937 out,
1938 "{indent}{export_prefix}type {name}{} = {};\n",
1939 generics_str(generics),
1940 ts_type(ty)
1941 );
1942 }
1943 TypeKind::Class {
1944 generics,
1945 fields,
1946 methods,
1947 method_visibility,
1948 statics,
1949 static_visibility,
1950 static_fields,
1951 accessors,
1952 constructor,
1953 constructor_visibility,
1954 ..
1955 } => {
1956 let extends = parent.map(|parent| format!(" extends {parent}"));
1961 let _ = writeln!(
1962 out,
1963 "{indent}{export_prefix}class {name}{}{} {{",
1964 generics_str(generics),
1965 extends.unwrap_or_default()
1966 );
1967 for (fname, field) in static_fields {
1968 if field.visibility == crate::Visibility::Private {
1969 continue;
1970 }
1971 push_doc(out, &field.doc, &inner);
1972 let ro = if field.readonly { "readonly " } else { "" };
1973 let fname = member_name(fname);
1974 let _ = writeln!(out, "{inner}static {ro}{fname}: {};", ts_type(&field.ty));
1975 }
1976 for (mname, m) in statics {
1977 if static_visibility.get(mname) == Some(&crate::Visibility::Private) {
1978 continue;
1979 }
1980 push_doc(out, &m.doc, &inner);
1981 let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1982 let mname = member_name(mname);
1983 let _ = writeln!(
1984 out,
1985 "{inner}static {mname}{}({}): {ret};",
1986 generics_str(&m.generics),
1987 ts_params_str(&m.params, m.doc.as_ref())
1988 );
1989 }
1990 for (fname, field) in fields {
1991 if field.visibility == crate::Visibility::Private
1992 || accessors.iter().any(|a| a.name() == fname)
1993 {
1994 continue;
1995 }
1996 push_doc(out, &field.doc, &inner);
1997 let ro = if field.readonly { "readonly " } else { "" };
1998 let opt = if field.optional { "?" } else { "" };
1999 let fname = member_name(fname);
2000 let _ = writeln!(out, "{inner}{ro}{fname}{opt}: {};", ts_type(&field.ty));
2001 }
2002 render_class_accessors(out, fields, accessors, &inner, ts_type);
2003 if *constructor_visibility == crate::Visibility::Private {
2009 let _ = writeln!(out, "{inner}protected constructor();");
2010 } else {
2011 let _ = writeln!(
2012 out,
2013 "{inner}constructor({});",
2014 ts_params_str(constructor, None)
2015 );
2016 }
2017 for (mname, m) in methods {
2018 if method_visibility.get(mname) == Some(&crate::Visibility::Private) {
2019 continue;
2020 }
2021 push_doc(out, &m.doc, &inner);
2022 let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
2023 let optional = if m.optional { "?" } else { "" };
2024 let mname = member_name(mname);
2025 let _ = writeln!(
2026 out,
2027 "{inner}{mname}{optional}{}({}): {ret};",
2028 generics_str(&m.generics),
2029 ts_params_str(&m.params, m.doc.as_ref())
2030 );
2031 }
2032 let _ = writeln!(out, "{indent}}}\n");
2033 }
2034 }
2035}
2036
2037fn render_class_accessors(
2042 out: &mut String,
2043 fields: &std::collections::BTreeMap<String, crate::FieldSig>,
2044 accessors: &[crate::AccessorSig],
2045 inner: &str,
2046 fmt_ty: impl Fn(&Type) -> String,
2047) {
2048 use crate::AccessorSig;
2049 let mut by_name: std::collections::BTreeMap<&str, (Option<&Type>, Option<&crate::Param>)> =
2050 std::collections::BTreeMap::new();
2051 for acc in accessors {
2052 let entry = by_name.entry(acc.name()).or_insert((None, None));
2053 match acc {
2054 AccessorSig::Getter { ret_ty, .. } => entry.0 = Some(ret_ty),
2055 AccessorSig::Setter { param, .. } => entry.1 = Some(param),
2056 }
2057 }
2058 for (name, (get, set)) in by_name {
2059 if fields.get(name).map(|f| f.visibility) == Some(crate::Visibility::Private) {
2060 continue;
2061 }
2062 let name = member_name(name);
2063 match (get, set) {
2064 (Some(r), Some(p)) if r == &p.ty => {
2065 let _ = writeln!(out, "{inner}{name}: {};", fmt_ty(r));
2066 }
2067 (Some(r), Some(p)) => {
2068 let _ = writeln!(out, "{inner}get {name}(): {};", fmt_ty(r));
2069 let _ = writeln!(out, "{inner}set {name}({}: {});", p.name, fmt_ty(&p.ty));
2070 }
2071 (Some(r), None) => {
2072 let _ = writeln!(out, "{inner}readonly {name}: {};", fmt_ty(r));
2073 }
2074 (None, Some(p)) => {
2075 let _ = writeln!(out, "{inner}set {name}({}: {});", p.name, fmt_ty(&p.ty));
2076 }
2077 (None, None) => {}
2078 }
2079 }
2080}
2081
2082fn render_ts_type_with_ctor(
2083 out: &mut String,
2084 types: &BTreeMap<String, TypeSymbol>,
2085 name: &str,
2086 indent: &str,
2087 export_prefix: &str,
2088 synthesize_binding: bool,
2089) {
2090 if let Some(sym) = types.get(name) {
2091 push_doc(out, type_doc(&sym.kind), indent);
2092 render_ts_type(out, name, &sym.kind, indent, export_prefix, None);
2093 }
2094 let ctor = format!("{name}Constructor");
2095 if let Some(sym) = types.get(&ctor) {
2096 push_doc(out, type_doc(&sym.kind), indent);
2097 render_ts_type(out, &ctor, &sym.kind, indent, export_prefix, None);
2098 if synthesize_binding {
2099 let _ = writeln!(out, "{indent}{export_prefix}const {name}: {ctor};\n");
2100 }
2101 }
2102}
2103
2104fn render_ts_namespace(
2105 out: &mut String,
2106 name: &str,
2107 ns: &NamespaceSymbol,
2108 indent: &str,
2109 export_prefix: &str,
2110) {
2111 push_doc(out, &ns.doc, indent);
2112 let _ = writeln!(out, "{indent}{export_prefix}namespace {name} {{");
2113 let inner = format!("{indent} ");
2114 for (vname, sym) in &ns.values {
2115 push_doc(out, value_doc(&sym.kind), &inner);
2116 render_ts_value(out, vname, &sym.kind, &inner, "");
2117 }
2118 for (tname, sym) in &ns.types {
2119 push_doc(out, type_doc(&sym.kind), &inner);
2120 render_ts_type(out, tname, &sym.kind, &inner, "", None);
2121 }
2122 for (nname, sub) in &ns.namespaces {
2123 render_ts_namespace(out, nname, sub, &inner, "");
2124 }
2125 let _ = writeln!(out, "{indent}}}\n");
2126}
2127
2128fn ts_params_str(params: &[Param], doc: Option<&DocComment>) -> String {
2129 rendered_params(params, doc, ts_type)
2130}
2131
2132fn ts_return_type(
2133 params: &[Param],
2134 ret: &Type,
2135 predicate: Option<&TypePredicate>,
2136 doc: Option<&DocComment>,
2137) -> String {
2138 let Some(predicate) = predicate else {
2139 return ts_type(ret);
2140 };
2141 let Some(param) = params.get(predicate.parameter_index as usize) else {
2142 return ts_type(ret);
2143 };
2144 let names = parameter_display_names(params, doc);
2145 let Some(name) = names.get(predicate.parameter_index as usize) else {
2146 return ts_type(ret);
2147 };
2148 let asserted = ts_type(&predicate.asserted_type);
2149 match param.ty {
2150 Type::TypeVar(_) | Type::GenericParam { .. } => {
2151 format!("{name} is {} & {asserted}", ts_type(¶m.ty))
2152 }
2153 _ => format!("{name} is {asserted}"),
2154 }
2155}
2156
2157fn ts_type(ty: &Type) -> String {
2158 match ty {
2159 Type::Refined { original, ty } => format!("({} & {})", ts_type(original), ts_type(ty)),
2160 Type::Number => "number".to_string(),
2161 Type::NumberLiteral(n) => n.0.to_string(),
2162 Type::BigInt => "bigint".to_string(),
2163 Type::BigIntLiteral(digits) => format!("{digits}n"),
2164 Type::String => "string".to_string(),
2165 Type::TemplateLiteral { .. } => ty.to_string(),
2166 Type::StringLiteral(s) => format!("\"{}\"", escape_string_literal(s)),
2167 Type::Uint8Array => "Uint8Array".to_string(),
2168 Type::Boolean => "boolean".to_string(),
2169 Type::BooleanLiteral(value) => value.to_string(),
2170 Type::Null => "null".to_string(),
2171 Type::Undefined => "undefined".to_string(),
2172 Type::Void => "void".to_string(),
2173 Type::Unknown => "unknown".to_string(),
2174 Type::NonPrimitive => "object".to_string(),
2175 Type::Function {
2176 params,
2177 ret,
2178 predicate,
2179 has_rest,
2180 optional,
2181 } => {
2182 let params = params
2183 .iter()
2184 .enumerate()
2185 .map(|(i, p)| {
2186 let prefix = if *has_rest && i == params.len().saturating_sub(1) {
2187 "..."
2188 } else {
2189 ""
2190 };
2191 let fixed = params.len().saturating_sub(usize::from(*has_rest));
2192 if i < fixed && i >= fixed.saturating_sub(*optional) {
2193 let shown = crate::types::shown_beside_optional_marker(p);
2194 format!("{prefix}arg{i}?: {}", ts_type(&shown))
2195 } else {
2196 format!("{prefix}arg{i}: {}", ts_type(p))
2197 }
2198 })
2199 .collect::<Vec<_>>()
2200 .join(", ");
2201 let ret = predicate.as_deref().map_or_else(
2202 || ts_type(ret),
2203 |p| format!("arg{} is {}", p.parameter_index, ts_type(&p.asserted_type)),
2204 );
2205 format!("({params}) => {ret}")
2206 }
2207 Type::Object { fields, index } => {
2208 let mut members: Vec<String> = fields
2209 .iter()
2210 .map(|(name, field)| {
2211 let opt = if field.optional { "?" } else { "" };
2212 let ro = if field.readonly { "readonly " } else { "" };
2213 let name = member_name(name);
2214 format!("{ro}{name}{opt}: {}", ts_type(&field.ty))
2215 })
2216 .collect();
2217 if let Some(index) = index {
2218 let ro = if index.readonly { "readonly " } else { "" };
2219 members.push(format!(
2220 "{ro}[key: {}]: {}",
2221 ts_type(&index.key),
2222 ts_type(&index.value)
2223 ));
2224 }
2225 if members.is_empty() {
2226 "{}".into()
2227 } else {
2228 format!("{{ {} }}", members.join("; "))
2229 }
2230 }
2231 Type::Array(elem) => format!("{}[]", ts_type_array_element(elem)),
2232 Type::Tuple(elements) => {
2233 let elements = elements
2234 .iter()
2235 .enumerate()
2236 .map(|(index, ty)| {
2237 if index >= elements.len().saturating_sub(elements.optional) {
2238 format!("({})?", ts_type(ty))
2239 } else {
2240 ts_type(ty)
2241 }
2242 })
2243 .collect::<Vec<_>>()
2244 .join(", ");
2245 format!("[{elements}]")
2246 }
2247 Type::Readonly(inner) if matches!(inner.as_ref(), Type::InterfaceRef { .. }) => {
2248 format!("Readonly{}", ts_type(inner))
2249 }
2250 Type::Readonly(inner) => format!("readonly {}", ts_type(inner)),
2251 Type::Error => "never".to_string(),
2252 Type::Never => "never".to_string(),
2253 Type::TypeVar(name) | Type::GenericParam { name, .. } => name.clone(),
2254 Type::KeyOf(_) | Type::IndexedAccess { .. } => ty.to_string(),
2255 Type::InterfaceRef { name, args, .. }
2256 | Type::ClassRef { name, args, .. }
2257 | Type::Alias { name, args, .. } => ts_named_type(name, args),
2258 Type::NumberEnum { name, .. } | Type::StringEnum { name, .. } => name.clone(),
2259 Type::Union(members) => crate::types::union_display_order(members)
2260 .into_iter()
2261 .map(ts_type_union_member)
2262 .collect::<Vec<_>>()
2263 .join(" | "),
2264 Type::AliasRef { name, args, .. } => ts_named_type(name, args),
2265 }
2266}
2267
2268fn ts_named_type(name: &str, args: &[Type]) -> String {
2269 if args.is_empty() {
2270 return name.to_string();
2271 }
2272 let args = args.iter().map(ts_type).collect::<Vec<_>>().join(", ");
2273 format!("{name}<{args}>")
2274}
2275
2276fn ts_type_array_element(ty: &Type) -> String {
2277 match ty {
2278 Type::Readonly(inner) if matches!(inner.as_ref(), Type::InterfaceRef { .. }) => ts_type(ty),
2279 Type::Function { .. } | Type::Union(_) | Type::Readonly(_) | Type::KeyOf(_) => {
2280 format!("({})", ts_type(ty))
2281 }
2282 _ => ts_type(ty),
2283 }
2284}
2285
2286fn ts_type_union_member(ty: &Type) -> String {
2287 match ty {
2288 Type::Function { .. } => format!("({})", ts_type(ty)),
2289 _ => ts_type(ty),
2290 }
2291}
2292
2293fn is_ts_reserved_word(name: &str) -> bool {
2294 matches!(
2295 name,
2296 "break"
2297 | "case"
2298 | "catch"
2299 | "class"
2300 | "const"
2301 | "continue"
2302 | "debugger"
2303 | "default"
2304 | "delete"
2305 | "do"
2306 | "else"
2307 | "enum"
2308 | "export"
2309 | "extends"
2310 | "false"
2311 | "finally"
2312 | "for"
2313 | "function"
2314 | "if"
2315 | "import"
2316 | "in"
2317 | "instanceof"
2318 | "new"
2319 | "null"
2320 | "return"
2321 | "super"
2322 | "switch"
2323 | "this"
2324 | "throw"
2325 | "true"
2326 | "try"
2327 | "typeof"
2328 | "var"
2329 | "void"
2330 | "while"
2331 | "with"
2332 | "as"
2333 | "async"
2334 | "await"
2335 | "from"
2336 | "get"
2337 | "let"
2338 | "of"
2339 | "set"
2340 | "static"
2341 | "using"
2342 | "yield"
2343 )
2344}
2345
2346fn json_package_declaration() -> PackageDeclaration {
2347 let mut defs = PackageDeclaration::with_package(crate::mangle::PRELUDE_PACKAGE);
2348 let json_prefix = crate::mangle::prelude(JSON_BUILTIN);
2349 let mut json = NamespaceSymbol {
2350 name: JSON_BUILTIN.to_string(),
2351 mangled_prefix: json_prefix.clone(),
2352 declaration_span: Span::at(FileId::JSON),
2353 values: BTreeMap::new(),
2354 types: BTreeMap::new(),
2355 namespaces: BTreeMap::new(),
2356 doc: None,
2357 };
2358 json.values.insert(
2359 "stringify".to_string(),
2360 ValueSymbol {
2361 name: "stringify".to_string(),
2362 mangled_name: crate::mangle::extend(&json_prefix, "stringify"),
2363 declaration_span: Span::at(FileId::JSON),
2364 kind: ValueKind::Function {
2365 generics: crate::GenericParameters::unconstrained(vec!["T".to_string()]),
2366 params: vec![
2367 Param::new("value", Type::TypeVar("T".to_string())),
2368 Param {
2369 name: "replacer".to_string(),
2370 ty: Type::union(vec![Type::Null, Type::Undefined]),
2371 default: Some(crate::DefaultValue::Undefined),
2372 optional: false,
2373 rest: false,
2374 },
2375 Param {
2376 name: "space".to_string(),
2377 ty: Type::union(vec![Type::Number, Type::String, Type::Null, Type::Undefined]),
2378 default: Some(crate::DefaultValue::Undefined),
2379 optional: false,
2380 rest: false,
2381 },
2382 ],
2383 ret: Type::String,
2384 type_predicate: None,
2385 doc: crate::doc(FileId::JSON, "/** Serialize a value to a JSON string. An `undefined` or function value has no JSON text: the call throws `MissingValueError` where the result is read as `string`, and is `undefined` at a check or where the type includes `undefined`. */"),
2386 },
2387 },
2388 );
2389 json.values.insert(
2390 "parse".to_string(),
2391 ValueSymbol {
2392 name: "parse".to_string(),
2393 mangled_name: crate::mangle::extend(&json_prefix, "parse"),
2394 declaration_span: Span::at(FileId::JSON),
2395 kind: ValueKind::Function {
2396 generics: Default::default(),
2397 params: vec![Param::new("text", Type::String)],
2398 ret: Type::Unknown,
2399 type_predicate: None,
2400 doc: crate::doc(
2401 FileId::JSON,
2402 "/** Parse a JSON string as unknown; use `JSON.parse(s) as T` to validate a target type. */",
2403 ),
2404 },
2405 },
2406 );
2407 defs.namespaces.insert(JSON_BUILTIN.to_string(), json);
2408 defs
2409}
2410
2411fn render_value(out: &mut String, name: &str, kind: &ValueKind, indent: &str) {
2412 match kind {
2413 ValueKind::Function {
2414 generics,
2415 params,
2416 ret,
2417 ..
2418 } => {
2419 let _ = writeln!(
2420 out,
2421 "{indent}function {name}{}({}): {ret};\n",
2422 generics_str(generics),
2423 params_str(params, value_doc(kind).as_ref())
2424 );
2425 }
2426 ValueKind::Let { ty, .. } => {
2427 let _ = writeln!(out, "{indent}let {name}: {ty};\n");
2428 }
2429 ValueKind::Const { ty, .. } => {
2430 let _ = writeln!(out, "{indent}const {name}: {ty};\n");
2431 }
2432 }
2433}
2434
2435fn render_type(out: &mut String, name: &str, sym: &TypeSymbol, indent: &str) {
2436 render_type_for(out, name, sym, indent, DocAudience::Author);
2437}
2438
2439fn render_type_for(
2440 out: &mut String,
2441 name: &str,
2442 sym: &TypeSymbol,
2443 indent: &str,
2444 audience: DocAudience,
2445) {
2446 let inner = format!("{indent} ");
2447 let parent = intrinsic_array_parent(sym)
2448 .map(|parent| format!(" extends {parent}"))
2449 .unwrap_or_default();
2450 match &sym.kind {
2451 TypeKind::Interface {
2452 generics,
2453 methods,
2454 properties,
2455 ..
2456 } => {
2457 let _ = writeln!(
2458 out,
2459 "{indent}interface {name}{}{parent} {{",
2460 generics_str(generics)
2461 );
2462 for (pname, prop) in properties {
2463 push_doc_for(out, &prop.doc, &inner, audience);
2464 let ro = if prop.readonly { "readonly " } else { "" };
2465 let opt = if prop.optional { "?" } else { "" };
2466 let pname = member_name(pname);
2467 let _ = writeln!(out, "{inner}{ro}{pname}{opt}: {};", prop.ty);
2468 }
2469 for (mname, m) in methods {
2470 push_doc_for(out, &m.doc, &inner, audience);
2471 let mname = method_prefix(mname);
2472 let _ = writeln!(
2473 out,
2474 "{inner}{mname}{}{}({}): {};",
2475 if m.optional { "?" } else { "" },
2476 generics_str(&m.generics),
2477 params_str(&m.params, m.doc.as_ref()),
2478 m.ret
2479 );
2480 }
2481 let _ = writeln!(out, "{indent}}}\n");
2482 }
2483 TypeKind::NumberEnum { variants, .. } => {
2484 let _ = writeln!(out, "{indent}enum {name} {{");
2485 for (v, value) in variants {
2486 let _ = writeln!(out, "{inner}{v} = {value},");
2487 }
2488 let _ = writeln!(out, "{indent}}}\n");
2489 }
2490 TypeKind::StringEnum { variants, .. } => {
2491 let _ = writeln!(out, "{indent}enum {name} {{");
2492 for (v, value) in variants {
2493 let _ = writeln!(out, "{inner}{v} = \"{}\",", escape_string_literal(value));
2494 }
2495 let _ = writeln!(out, "{indent}}}\n");
2496 }
2497 TypeKind::Alias { ty, .. } if let Some(text) = utility_declaration(name, ty) => {
2498 let _ = writeln!(out, "{indent}{text}\n");
2499 }
2500 TypeKind::Alias { generics, ty, .. } => {
2501 let _ = writeln!(
2502 out,
2503 "{indent}type {name}{} = {ty};\n",
2504 generics_str(generics)
2505 );
2506 }
2507 TypeKind::Class {
2508 generics,
2509 fields,
2510 methods,
2511 method_visibility,
2512 statics,
2513 static_visibility,
2514 static_fields,
2515 accessors,
2516 constructor,
2517 constructor_visibility,
2518 ..
2519 } => {
2520 let _ = writeln!(out, "{indent}class {name}{} {{", generics_str(generics));
2525 for (fname, field) in static_fields {
2526 if field.visibility == crate::Visibility::Private {
2527 continue;
2528 }
2529 push_doc_for(out, &field.doc, &inner, audience);
2530 let ro = if field.readonly { "readonly " } else { "" };
2531 let fname = member_name(fname);
2532 let _ = writeln!(out, "{inner}static {ro}{fname}: {};", field.ty);
2533 }
2534 for (mname, m) in statics {
2535 if static_visibility.get(mname) == Some(&crate::Visibility::Private) {
2536 continue;
2537 }
2538 push_doc_for(out, &m.doc, &inner, audience);
2539 let mname = member_name(mname);
2540 let _ = writeln!(
2541 out,
2542 "{inner}static {mname}{}({}): {};",
2543 generics_str(&m.generics),
2544 params_str(&m.params, m.doc.as_ref()),
2545 m.ret
2546 );
2547 }
2548 for (fname, field) in fields {
2549 if field.visibility == crate::Visibility::Private
2550 || accessors.iter().any(|a| a.name() == fname)
2551 {
2552 continue;
2553 }
2554 push_doc_for(out, &field.doc, &inner, audience);
2555 let ro = if field.readonly { "readonly " } else { "" };
2556 let opt = if field.optional { "?" } else { "" };
2557 let fname = member_name(fname);
2558 let _ = writeln!(out, "{inner}{ro}{fname}{opt}: {};", field.ty);
2559 }
2560 render_class_accessors(out, fields, accessors, &inner, ToString::to_string);
2561 if *constructor_visibility != crate::Visibility::Private {
2562 let _ = writeln!(
2563 out,
2564 "{inner}constructor({});",
2565 params_str(constructor, None)
2566 );
2567 }
2568 for (mname, m) in methods {
2569 if method_visibility.get(mname) == Some(&crate::Visibility::Private) {
2570 continue;
2571 }
2572 push_doc_for(out, &m.doc, &inner, audience);
2573 let mname = member_name(mname);
2574 let _ = writeln!(
2575 out,
2576 "{inner}{mname}{}{}({}): {};",
2577 if m.optional { "?" } else { "" },
2578 generics_str(&m.generics),
2579 params_str(&m.params, m.doc.as_ref()),
2580 m.ret
2581 );
2582 }
2583 let _ = writeln!(out, "{indent}}}\n");
2584 }
2585 }
2586}
2587
2588fn render_type_with_ctor(
2593 out: &mut String,
2594 types: &BTreeMap<String, TypeSymbol>,
2595 name: &str,
2596 indent: &str,
2597) {
2598 if let Some(sym) = types.get(name) {
2599 push_doc(out, type_doc(&sym.kind), indent);
2600 render_type(out, name, sym, indent);
2601 }
2602 let ctor = format!("{name}Constructor");
2603 if let Some(sym) = types.get(&ctor) {
2604 push_doc(out, type_doc(&sym.kind), indent);
2605 render_type(out, &ctor, sym, indent);
2606 let _ = writeln!(out, "{indent}const {name}: {ctor};\n");
2607 }
2608}
2609
2610fn render_namespace(out: &mut String, name: &str, ns: &NamespaceSymbol, indent: &str) {
2611 let _ = writeln!(out, "{indent}namespace {name} {{");
2612 let inner = format!("{indent} ");
2613 for (vname, sym) in &ns.values {
2614 push_doc(out, value_doc(&sym.kind), &inner);
2615 render_value(out, vname, &sym.kind, &inner);
2616 }
2617 for (tname, sym) in &ns.types {
2618 push_doc(out, type_doc(&sym.kind), &inner);
2621 render_type(out, tname, sym, &inner);
2622 }
2623 for (nname, sub) in &ns.namespaces {
2624 render_namespace(out, nname, sub, &inner);
2625 }
2626 let _ = writeln!(out, "{indent}}}\n");
2627}
2628
2629fn generics_str(generics: &crate::GenericParameters) -> String {
2630 if generics.is_empty() {
2631 String::new()
2632 } else {
2633 format!(
2634 "<{}>",
2635 generics
2636 .iter()
2637 .map(|name| match generics.constraints.get(name) {
2638 Some(constraint) => format!("{name} extends {constraint}"),
2639 None => name.clone(),
2640 })
2641 .collect::<Vec<_>>()
2642 .join(", ")
2643 )
2644 }
2645}
2646
2647fn ts_interface_generics(
2651 name: &str,
2652 generics: &crate::GenericParameters,
2653 export_prefix: &str,
2654) -> String {
2655 if export_prefix == "declare "
2656 && matches!(name, "Iterable" | "Iterator")
2657 && let [t] = generics.names.as_slice()
2658 {
2659 return format!("<{t}, TReturn = any, TNext = any>");
2660 }
2661 generics_str(generics)
2662}
2663
2664fn params_str(params: &[Param], doc: Option<&DocComment>) -> String {
2665 rendered_params(params, doc, ToString::to_string)
2666}
2667
2668fn rendered_params(
2669 params: &[Param],
2670 doc: Option<&DocComment>,
2671 format_type: impl Fn(&Type) -> String,
2672) -> String {
2673 let names = parameter_display_names(params, doc);
2674 params
2675 .iter()
2676 .zip(names)
2677 .enumerate()
2678 .map(|(index, (p, name))| {
2679 let prefix = if p.rest { "..." } else { "" };
2680 let later_required = params
2682 .iter()
2683 .skip(index.saturating_add(1))
2684 .any(|later| !later.is_omittable() && !later.rest);
2685 let opt = if p.is_omittable() && !later_required {
2686 "?"
2687 } else {
2688 ""
2689 };
2690 let ty = if opt.is_empty() {
2692 p.ty.clone()
2693 } else {
2694 crate::types::shown_beside_optional_marker(&p.ty)
2695 };
2696 format!("{prefix}{name}{opt}: {}", format_type(&ty))
2697 })
2698 .collect::<Vec<_>>()
2699 .join(", ")
2700}
2701
2702fn parameter_display_names(params: &[Param], doc: Option<&DocComment>) -> Vec<String> {
2705 let tags: Vec<_> = doc
2706 .into_iter()
2707 .flat_map(|doc| &doc.params)
2708 .filter(|tag| !tag.name.contains('.'))
2709 .collect();
2710 let mut used: BTreeSet<String> = params
2711 .iter()
2712 .filter(|p| !crate::lower_patterns::is_pattern_param(&p.name))
2713 .map(|p| p.name.clone())
2714 .collect();
2715 params
2716 .iter()
2717 .enumerate()
2718 .map(|(index, param)| {
2719 if !crate::lower_patterns::is_pattern_param(¶m.name) {
2720 return param.name.clone();
2721 }
2722 if let Some(tag) = tags.get(index)
2723 && valid_parameter_name(&tag.name)
2724 && used.insert(tag.name.clone())
2725 {
2726 return tag.name.clone();
2727 }
2728 let mut name = format!("arg{index}");
2729 while !used.insert(name.clone()) {
2730 name.push('_');
2731 }
2732 name
2733 })
2734 .collect()
2735}
2736
2737fn valid_parameter_name(name: &str) -> bool {
2738 let mut chars = name.chars();
2739 chars
2740 .next()
2741 .is_some_and(|c| c.is_ascii_alphabetic() || matches!(c, '_' | '$'))
2742 && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '$'))
2743 && !is_ts_reserved_word(name)
2744 && !matches!(
2745 name,
2746 "arguments"
2747 | "eval"
2748 | "implements"
2749 | "interface"
2750 | "package"
2751 | "private"
2752 | "protected"
2753 | "public"
2754 )
2755}
2756
2757fn push_doc(out: &mut String, doc: &Option<DocComment>, indent: &str) {
2758 push_doc_for(out, doc, indent, DocAudience::Author);
2759}
2760
2761fn push_doc_for(out: &mut String, doc: &Option<DocComment>, indent: &str, audience: DocAudience) {
2762 let Some(doc) = doc else { return };
2763 let mut lines: Vec<String> = doc.summary.lines().map(str::to_string).collect();
2764 for p in &doc.params {
2765 lines.push(format!("@param {} {}", p.name, p.description));
2766 }
2767 let capabilities = match audience {
2768 DocAudience::Author => doc.capabilities.as_slice(),
2769 DocAudience::Agent => &[],
2770 };
2771 for cap in capabilities {
2772 let mut line = format!("@capability {}", cap.capability);
2773 if !cap.bindings.is_empty() {
2774 let bindings = cap
2775 .bindings
2776 .iter()
2777 .map(|b| {
2778 let value = match &b.kind {
2779 DocCapabilityBindingKind::Parameter { param, path, .. }
2780 if *param == b.field && path.is_empty() =>
2781 {
2782 String::new()
2783 }
2784 DocCapabilityBindingKind::Parameter { param, path, .. } => {
2785 let suffix = path
2786 .iter()
2787 .map(|part| format!(".{part}"))
2788 .collect::<String>();
2789 format!(": ${param}{suffix}")
2790 }
2791 DocCapabilityBindingKind::Type { name, .. } => format!(": {name}"),
2792 DocCapabilityBindingKind::Literal { value, .. } => {
2793 format!(": {}", literal_str(value))
2794 }
2795 };
2796 format!("{}{}", b.field, value)
2797 })
2798 .collect::<Vec<_>>()
2799 .join(", ");
2800 line.push_str(&format!(" {{ {bindings} }}"));
2801 }
2802 if !cap.description.is_empty() {
2803 line.push_str(" - ");
2804 line.push_str(&cap.description);
2805 }
2806 lines.push(line);
2807 }
2808 if let Some(r) = &doc.returns {
2809 lines.push(format!("@returns {}", r.description));
2810 }
2811 if lines.is_empty() {
2812 return;
2813 }
2814 let _ = writeln!(out, "{indent}/**");
2815 for line in lines {
2816 let _ = writeln!(out, "{indent} * {line}");
2817 }
2818 let _ = writeln!(out, "{indent} */");
2819}
2820
2821fn literal_str(value: &DocCapabilityLiteral) -> String {
2822 match value {
2823 DocCapabilityLiteral::String(s) => {
2824 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
2825 }
2826 DocCapabilityLiteral::Number(n) => n.clone(),
2827 DocCapabilityLiteral::Boolean(v) => v.to_string(),
2828 DocCapabilityLiteral::Null => "null".to_string(),
2829 }
2830}
2831
2832fn value_doc(kind: &ValueKind) -> &Option<DocComment> {
2833 match kind {
2834 ValueKind::Function { doc, .. }
2835 | ValueKind::Let { doc, .. }
2836 | ValueKind::Const { doc, .. } => doc,
2837 }
2838}
2839
2840fn type_doc(kind: &TypeKind) -> &Option<DocComment> {
2841 match kind {
2842 TypeKind::Interface { doc, .. }
2843 | TypeKind::Class { doc, .. }
2844 | TypeKind::NumberEnum { doc, .. }
2845 | TypeKind::StringEnum { doc, .. }
2846 | TypeKind::Alias { doc, .. } => doc,
2847 }
2848}
2849
2850fn method_prefix(name: &str) -> Cow<'_, str> {
2854 match name {
2855 "@call" => Cow::Borrowed(""),
2856 "new" => Cow::Borrowed("new "),
2857 _ => member_name(name),
2858 }
2859}
2860
2861fn member_name(name: &str) -> Cow<'_, str> {
2866 if name.is_ascii() && crate::type_rendering::is_identifier_name(name) {
2867 return Cow::Borrowed(name);
2868 }
2869 Cow::Owned(format!("\"{}\"", escape_string_literal(name)))
2870}
2871
2872#[cfg(test)]
2873mod tests {
2874 use super::*;
2875
2876 #[test]
2877 fn member_names_are_quoted_unless_identifiers() {
2878 let mut lone = String::new();
2879 crate::literal_units::push_lone_surrogate(&mut lone, 0xD800);
2880 assert_eq!(member_name("count"), "count");
2881 assert_eq!(member_name("$el_2"), "$el_2");
2882 assert_eq!(member_name("café"), "\"café\"");
2883 assert_eq!(member_name("a b"), "\"a b\"");
2884 assert_eq!(member_name("0"), "\"0\"");
2885 assert_eq!(member_name(&lone), "\"\\uD800\"");
2886 }
2887
2888 #[test]
2889 fn destructured_display_names_are_valid_and_distinct() {
2890 let params = [
2891 Param::new("#pattern_p_0", Type::Number),
2892 Param::new("arg0", Type::Number),
2893 Param::new("", Type::Number),
2894 Param::new("#pattern_p_3", Type::Number),
2895 ];
2896 let doc = crate::doc(
2897 FileId(0),
2898 "/** Names.\n * @param class Reserved.\n * @param arg0 Existing.\n * @param arg0.x Property.\n * @param pair Pair.\n * @param pair Duplicate.\n */",
2899 );
2900 assert_eq!(
2901 parameter_display_names(¶ms, doc.as_ref()),
2902 ["arg0_", "arg0", "pair", "arg3"]
2903 );
2904 assert_eq!(
2905 params_str(¶ms, doc.as_ref()),
2906 "arg0_: number, arg0: number, pair: number, arg3: number"
2907 );
2908 assert_eq!(
2909 ts_params_str(¶ms, doc.as_ref()),
2910 "arg0_: number, arg0: number, pair: number, arg3: number"
2911 );
2912 }
2913
2914 #[test]
2915 fn catalog_filters_before_counting_omitted_entries() {
2916 let extra = (0..CATALOG_LIMIT)
2917 .map(|index| CatalogEntry {
2918 name: format!("@pied-piper/package{index}"),
2919 source: "registry".into(),
2920 description: String::new(),
2921 })
2922 .collect();
2923 let catalog = catalog_filtered(extra, |name| !name.starts_with("submilli:"));
2924 assert_eq!(catalog.entries.len(), CATALOG_LIMIT);
2925 assert_eq!(catalog.remaining, 0);
2926 assert!(
2927 catalog
2928 .entries
2929 .iter()
2930 .all(|entry| entry.name.starts_with("@pied-piper/"))
2931 );
2932 }
2933
2934 #[test]
2935 fn agent_declarations_omit_capability_metadata_without_changing_docs_or_symbols() {
2936 let mut defs = crate::compile_package(
2937 "@test/notes",
2938 crate::ModulePath::from("lib"),
2939 &[crate::PackageSourceModule {
2940 path: crate::ModulePath::from("lib"),
2941 source: r#"
2942 /** Read a note.
2943 * @param path The note path.
2944 * @capability notes.test/read { path }
2945 * @capability notes.test/audit
2946 * @returns The note.
2947 */
2948 export function read(path: string): string { return path; }
2949 export interface Notes {
2950 /** @capability notes.test/read */
2951 read(): string;
2952 }
2953 export class Client {
2954 /** Read through a client. Requires approval.
2955 * @capability notes.test/read */
2956 read(): string { return "note"; }
2957 /** @capability notes.test/write */
2958 static write(): void {}
2959 }
2960 "#,
2961 }],
2962 &[],
2963 )
2964 .unwrap()
2965 .declaration;
2966 let ValueKind::Function { doc: Some(doc), .. } =
2967 &mut defs.values.get_mut("read").unwrap().kind
2968 else {
2969 panic!("read has documentation");
2970 };
2971 doc.summary
2973 .push_str("\n@capability is metadata, not a parameter.");
2974 let original = defs.clone();
2975 let author = render_declarations(&defs);
2976 let agent = render_declarations_for(&defs, DocAudience::Agent);
2977 for capability in ["notes.test/read", "notes.test/audit", "notes.test/write"] {
2978 assert!(
2979 author.contains(&format!("@capability {capability}")),
2980 "{author}"
2981 );
2982 assert!(
2983 !agent.contains(&format!("@capability {capability}")),
2984 "{agent}"
2985 );
2986 }
2987 for preserved in [
2988 "@capability is metadata, not a parameter.",
2989 "@param path The note path.",
2990 "@returns The note.",
2991 "Requires approval.",
2992 "function read(path: string): string;",
2993 "read(): string;",
2994 "static write(): void;",
2995 ] {
2996 assert!(agent.contains(preserved), "{agent}");
2997 }
2998 assert!(!agent.contains("/**\n */"), "{agent}");
2999 assert!(!agent.contains("/**\n */"), "{agent}");
3000 assert_eq!(defs, original);
3001 assert_eq!(render_declarations(&defs), author);
3002 }
3003
3004 #[test]
3005 fn stdlib_agent_docs_and_bare_names_omit_capability_metadata() {
3006 let view = Stdlib::core().view(&Unscoped);
3007 for name in view.names() {
3008 let author = view.docs(name).unwrap();
3009 let agent = view.docs_for(name, DocAudience::Agent).unwrap();
3010 assert!(
3011 !agent.declarations.contains(" * @capability "),
3012 "{name}: {}",
3013 agent.declarations
3014 );
3015 assert_eq!(agent.description, author.description);
3016 assert_eq!(agent.name, author.name);
3017 }
3018 let BareNameMatch::Unique(agent) =
3019 view.match_bare_name_for("http", &[], DocAudience::Agent)
3020 else {
3021 panic!("http uniquely names submilli:http");
3022 };
3023 assert!(!agent.declarations.contains("@capability"));
3024 assert!(
3025 view.docs("submilli:http")
3026 .unwrap()
3027 .declarations
3028 .contains("@capability http.get")
3029 );
3030 }
3031
3032 #[test]
3033 fn http_docs_render_signatures() {
3034 let doc = docs("submilli:http").expect("http module");
3035 assert_eq!(doc.name, "submilli:http");
3036 assert!(!doc.description.is_empty());
3037 assert!(doc.declarations.contains("function get("));
3038 assert!(doc.declarations.contains("function post("));
3039 assert!(doc.declarations.contains("@capability http.get"));
3040 assert!(doc.declarations.contains("@capability http.download"));
3041 }
3042
3043 #[test]
3044 fn fs_and_package_configuration_docs_render_capabilities() {
3045 let fs = docs("submilli:fs").expect("fs module");
3046 assert!(fs.declarations.contains("@capability fs.read"));
3047 assert!(fs.declarations.contains("@capability fs.write"));
3048 assert!(fs.declarations.contains("@capability fs.list"));
3049
3050 let secrets = docs("submilli:secrets").expect("secrets module");
3051 assert!(secrets.declarations.contains("@capability secrets.get"));
3052 let variables = docs("submilli:variables").expect("variables module");
3053 assert!(variables.declarations.contains("@capability variables.get"));
3054 assert!(
3055 variables
3056 .declarations
3057 .contains("get(name: string): string | undefined")
3058 );
3059 }
3060
3061 #[test]
3062 fn security_is_not_user_facing() {
3063 assert!(docs("submilli:security").is_none());
3064 assert!(!search("").iter().any(|m| m.name == INTERNAL_MODULE));
3065 }
3066
3067 #[test]
3068 fn search_matches_symbol_names() {
3069 let hits = search("sha256");
3070 assert!(
3071 hits.iter().any(|m| m.name == "submilli:crypto"),
3072 "{:?}",
3073 names(&hits)
3074 );
3075 }
3076
3077 #[test]
3078 fn empty_query_lists_all_user_modules() {
3079 let names = names(&search(""));
3080 assert!(names.contains(&"submilli:crypto".to_string()));
3081 assert!(names.contains(&"submilli:http".to_string()));
3082 assert!(!names.contains(&INTERNAL_MODULE.to_string()));
3083 }
3084
3085 fn names(hits: &[ModuleSummary]) -> Vec<String> {
3086 hits.iter().map(|m| m.name.clone()).collect()
3087 }
3088
3089 #[test]
3090 fn builtins_lists_headline_types_and_namespaces() {
3091 let b = builtins();
3092 for expected in ["Array", "Map", "Set", "String", "Number", "Error"] {
3093 assert!(
3094 b.types.contains(&expected.to_string()),
3095 "missing {expected}"
3096 );
3097 }
3098 for hidden in [
3100 "ArrayConstructor",
3101 "Console",
3102 "Iterator",
3103 "Temporal#Instant",
3104 ] {
3105 assert!(!b.types.contains(&hidden.to_string()), "leaked {hidden}");
3106 }
3107 for ns in ["Math", "Temporal", "JSON"] {
3108 assert!(
3109 b.namespaces.contains(&ns.to_string()),
3110 "missing namespace {ns}"
3111 );
3112 }
3113 }
3114
3115 #[test]
3116 fn every_builtin_has_renderable_docs() {
3117 let b = builtins();
3118 for name in b.types.iter().chain(b.namespaces.iter()) {
3119 assert!(
3120 builtin_docs(name).is_some_and(|d| !d.is_empty()),
3121 "no docs for built-in {name}"
3122 );
3123 }
3124 }
3125
3126 #[test]
3127 fn dotted_path_returns_the_member_slice() {
3128 let slice = builtin_docs("Temporal.Instant").expect("Temporal.Instant");
3129 assert!(slice.contains("const Instant: Temporal.InstantConstructor;"));
3132 assert!(slice.contains("interface Instant {"));
3133 assert!(slice.contains("interface InstantConstructor {"));
3134 assert!(slice.starts_with("namespace Temporal {"), "{slice}");
3136 assert!(slice.contains("A point in time, to nanosecond precision"));
3138 assert!(!slice.contains("interface ZonedDateTime {"), "{slice}");
3140
3141 let full = builtin_docs("Temporal").expect("Temporal");
3142 assert!(
3143 slice.len() * 4 < full.len(),
3144 "slice {} vs full {}",
3145 slice.len(),
3146 full.len()
3147 );
3148 }
3149
3150 #[test]
3151 fn dotted_path_resolves_nested_namespaces_and_values() {
3152 let now = builtin_docs("Temporal.Now").expect("Temporal.Now");
3153 assert!(now.starts_with("namespace Temporal {"));
3154 assert!(now.contains("namespace Now {"));
3155 assert!(now.contains("function instant(): Temporal.Instant;"));
3156
3157 let one = builtin_docs("Temporal.Now.instant").expect("Temporal.Now.instant");
3158 assert!(one.contains("function instant(): Temporal.Instant;"));
3159 assert!(!one.contains("plainDateISO"), "{one}");
3160
3161 let max = builtin_docs("Math.max").expect("Math.max");
3162 assert!(max.contains("function max("), "{max}");
3163 assert!(!max.contains("function min("), "{max}");
3164
3165 let parse = builtin_docs("JSON.parse").expect("JSON.parse");
3166 assert!(parse.contains("function parse("), "{parse}");
3167 assert!(!parse.contains("function stringify("), "{parse}");
3168 }
3169
3170 #[test]
3171 fn dotted_path_resolves_members_of_a_type_head() {
3172 let is_array = builtin_docs("Array.isArray").expect("Array.isArray");
3175 assert!(
3176 is_array.starts_with("interface ArrayConstructor"),
3177 "{is_array}"
3178 );
3179 assert!(is_array.contains("isArray"));
3180
3181 let repeat = builtin_docs("String.repeat").expect("String.repeat");
3182 assert!(repeat.starts_with("interface String"), "{repeat}");
3183 assert!(
3184 repeat.contains("repeat(count: number): string;"),
3185 "{repeat}"
3186 );
3187
3188 let from = builtin_docs("Temporal.Instant.from").expect("Temporal.Instant.from");
3191 assert!(from.starts_with("namespace Temporal {"), "{from}");
3192 assert!(from.contains("interface InstantConstructor {"), "{from}");
3193 assert!(!from.contains("namespace Instant"), "{from}");
3194 }
3195
3196 #[test]
3197 fn every_member_a_miss_names_is_itself_resolvable() {
3198 let b = builtins();
3202 for name in b.types.iter().chain(b.namespaces.iter()) {
3203 let BuiltinLookup::UnknownMember { members, .. } =
3204 builtin_lookup(&format!("{name}.__definitely_not_a_member"))
3205 else {
3206 continue;
3207 };
3208 for member in members {
3209 let path = format!("{name}.{member}");
3210 assert!(
3211 matches!(builtin_lookup(&path), BuiltinLookup::Found(_)),
3212 "`{path}` is offered as a member of `{name}` but does not resolve"
3213 );
3214 }
3215 }
3216 }
3217
3218 #[test]
3219 fn unknown_member_lists_the_members_that_exist() {
3220 let BuiltinLookup::UnknownMember {
3221 path,
3222 member,
3223 members,
3224 } = builtin_lookup("Temporal.Foo")
3225 else {
3226 panic!("expected an unknown-member miss");
3227 };
3228 assert_eq!(path, "Temporal");
3229 assert_eq!(member, "Foo");
3230 assert!(members.contains(&"Instant".to_string()));
3231 assert!(members.contains(&"Now".to_string()));
3232 assert!(
3234 !members.contains(&"InstantConstructor".to_string()),
3235 "{members:?}"
3236 );
3237
3238 let BuiltinLookup::UnknownMember { path, members, .. } = builtin_lookup("Math.max.foo")
3241 else {
3242 panic!("expected an unknown-member miss");
3243 };
3244 assert_eq!(path, "Math.max");
3245 assert!(members.is_empty());
3246 }
3247
3248 #[test]
3249 fn a_bad_member_is_reported_against_its_full_path() {
3250 for (query, expected_path) in [
3251 ("Temporal.Instant.nope", "Temporal.Instant"),
3252 ("Temporal.Now.nope", "Temporal.Now"),
3253 ("Array.nope", "Array"),
3254 ("Temporal.Instant.from.x", "Temporal.Instant.from"),
3255 ("Array.isArray.foo", "Array.isArray"),
3258 ] {
3259 let BuiltinLookup::UnknownMember { path, member, .. } = builtin_lookup(query) else {
3260 panic!("expected an unknown-member miss for {query}");
3261 };
3262 assert_eq!(path, expected_path, "for {query}");
3263 assert!(!member.is_empty(), "for {query}");
3264 }
3265 }
3266
3267 #[test]
3268 fn unknown_head_is_not_an_unknown_member() {
3269 assert_eq!(builtin_lookup("Bogus.Thing"), BuiltinLookup::Unknown);
3270 assert_eq!(builtin_lookup("Temporal#Instant"), BuiltinLookup::Unknown);
3272 assert_eq!(builtin_lookup(""), BuiltinLookup::Unknown);
3273 }
3274
3275 #[test]
3276 fn plain_names_are_unchanged_and_segments_are_case_insensitive() {
3277 let full = builtin_docs("Temporal").expect("Temporal");
3279 assert!(full.starts_with("namespace Temporal {"));
3280 for member in ["Instant", "ZonedDateTime", "PlainDate", "Duration"] {
3281 assert!(
3282 full.contains(&format!("interface {member} {{")),
3283 "missing {member}"
3284 );
3285 }
3286 assert_eq!(
3287 builtin_docs("temporal.instant"),
3288 builtin_docs("Temporal.Instant")
3289 );
3290 assert_eq!(builtin_docs("Temporal."), builtin_docs("Temporal"));
3292 }
3293
3294 #[test]
3295 fn resolve_prefers_a_module_then_falls_back_to_builtins() {
3296 assert!(matches!(resolve("submilli:http"), Resolution::Module(_)));
3297 assert!(matches!(resolve("Temporal"), Resolution::Builtin { .. }));
3298 assert!(matches!(
3299 resolve("Temporal.Instant"),
3300 Resolution::Builtin { .. }
3301 ));
3302 assert!(matches!(
3303 resolve("Temporal.Foo"),
3304 Resolution::UnknownMember { .. }
3305 ));
3306 assert!(matches!(resolve("nope"), Resolution::Unknown));
3307 assert!(matches!(resolve("submilli:security"), Resolution::Unknown));
3308 }
3309
3310 #[test]
3311 fn suggest_reaches_past_bare_edit_distance() {
3312 let extra = vec!["@mcp/linear".to_string()];
3313 assert_eq!(suggest("http", &extra).as_deref(), Some("submilli:http"));
3316 assert_eq!(suggest("fs", &extra).as_deref(), Some("submilli:fs"));
3319 assert_eq!(suggest("FS", &extra).as_deref(), Some("submilli:fs"));
3320 assert_eq!(suggest("htp", &extra).as_deref(), Some("submilli:http"));
3321 assert_eq!(suggest("linear", &extra).as_deref(), Some("@mcp/linear"));
3322 assert_eq!(suggest("Temporel", &extra).as_deref(), Some("Temporal"));
3324 assert_eq!(
3325 suggest("Temporel.Instant", &extra).as_deref(),
3326 Some("Temporal")
3327 );
3328 assert_eq!(suggest("Aray", &extra).as_deref(), Some("Array"));
3329 assert_eq!(suggest("zzzzzzzzz", &extra), None);
3330 assert_ne!(
3333 suggest(INTERNAL_MODULE, &[]).as_deref(),
3334 Some(INTERNAL_MODULE)
3335 );
3336 }
3337
3338 #[test]
3339 fn a_deliberately_omitted_global_points_at_its_replacement() {
3340 assert_eq!(suggest("Date", &[]).as_deref(), Some("Temporal"));
3343 assert_eq!(suggest("date", &[]).as_deref(), Some("Temporal"));
3344 }
3345
3346 #[test]
3347 fn a_bare_stdlib_name_is_that_module_and_says_the_asked_name_is_wrong() {
3348 let view = Stdlib::core().view(&crate::docs_view::Unscoped);
3349 let BareNameMatch::Unique(doc) = view.match_bare_name("fs", &[]) else {
3350 panic!("fs is the tail of submilli:fs");
3351 };
3352 assert_eq!(doc.name, "submilli:fs");
3353 let BareNameMatch::Unique(doc) = view.match_bare_name("FS", &[]) else {
3354 panic!("the tail match is case-insensitive");
3355 };
3356 assert_eq!(doc.name, "submilli:fs");
3357 assert!(matches!(
3360 view.match_bare_name("submilli:fs", &[]),
3361 BareNameMatch::Miss
3362 ));
3363 assert!(matches!(
3364 view.match_bare_name("fs.readText", &[]),
3365 BareNameMatch::Miss
3366 ));
3367 assert!(matches!(
3368 view.match_bare_name("nope", &[]),
3369 BareNameMatch::Miss
3370 ));
3371 let BareNameMatch::Ambiguous(names) = view.match_bare_name("fs", &["@acme/fs".to_string()])
3374 else {
3375 panic!("two packages share the tail");
3376 };
3377 assert_eq!(
3378 names,
3379 vec!["@acme/fs".to_string(), "submilli:fs".to_string()]
3380 );
3381 let note = bare_name_note("fs", "submilli:fs");
3382 assert!(note.contains("submilli:fs"), "{note}");
3383 assert!(note.contains("Do not import `fs`"), "{note}");
3384 assert!(note.contains("does not exist"), "{note}");
3385 }
3386
3387 #[test]
3388 fn suggest_declines_degenerate_and_self_queries() {
3389 for q in ["", ".", "a", "xy"] {
3392 assert_eq!(suggest(q, &[]), None, "for {q:?}");
3393 }
3394 assert_eq!(suggest(".Temporel", &[]).as_deref(), Some("Temporal"));
3396 let declared = vec!["@pied-piper/mypkg".to_string()];
3398 assert_eq!(suggest("@pied-piper/mypkg", &declared), None);
3399 }
3400
3401 #[test]
3402 fn catalog_is_summary_only_and_bounded() {
3403 let stdlib = catalog(Vec::new());
3404 assert_eq!(stdlib.remaining, 0);
3405 assert!(stdlib.entries.iter().all(|e| e.source == SOURCE_STDLIB));
3406 assert!(stdlib.entries.iter().any(|e| e.name == "submilli:http"));
3407 assert!(stdlib.entries.iter().all(|e| !e.description.is_empty()));
3408 assert!(!stdlib.entries.iter().any(|e| e.name == INTERNAL_MODULE));
3409
3410 let extra: Vec<CatalogEntry> = (0..CATALOG_LIMIT)
3411 .map(|i| CatalogEntry {
3412 name: format!("@pied-piper/p{i}"),
3413 source: "registry".to_string(),
3414 description: "x".to_string(),
3415 })
3416 .collect();
3417 let big = catalog(extra);
3418 assert_eq!(big.entries.len(), CATALOG_LIMIT);
3419 assert_eq!(big.remaining, stdlib.entries.len());
3420 }
3421
3422 #[test]
3423 fn miss_messages_name_the_repair() {
3424 let msg = unknown_member_message("Temporal", "Foo", &["Instant".into(), "Now".into()]);
3425 assert!(msg.contains("Temporal"), "{msg}");
3426 assert!(msg.contains("Instant, Now"), "{msg}");
3427 let leaf = unknown_member_message("Math.max", "foo", &[]);
3428 assert!(leaf.contains("no members"), "{leaf}");
3429 assert!(builtin_no_import_note("Temporal").contains("import"));
3431 }
3432
3433 fn every_package() -> Stdlib {
3436 crate::stdlib::OptionalPackage::ALL
3437 .iter()
3438 .fold(Stdlib::core(), |set, package| set.with(*package))
3439 }
3440
3441 #[test]
3442 fn stdlib_capability_tags_parse_cleanly() {
3443 for module in user_modules(every_package()) {
3444 for symbol in module.values.values() {
3445 let Some(doc) = value_doc(&symbol.kind) else {
3446 continue;
3447 };
3448 for cap in &doc.capabilities {
3449 assert!(
3450 cap.diagnostics.is_empty(),
3451 "{}.{}: {:?}",
3452 module.package_name,
3453 symbol.name,
3454 cap.diagnostics
3455 );
3456 }
3457 }
3458 }
3459 }
3460
3461 #[test]
3462 fn every_importable_stdlib_symbol_has_docs() {
3463 for module in user_modules(every_package()) {
3464 for symbol in module.values.values() {
3465 assert!(
3466 value_doc(&symbol.kind).is_some(),
3467 "{}.{} is missing docs",
3468 module.package_name,
3469 symbol.name
3470 );
3471 }
3472 for symbol in module.types.values() {
3473 assert!(
3474 type_doc(&symbol.kind).is_some(),
3475 "{}.{} is missing docs",
3476 module.package_name,
3477 symbol.name
3478 );
3479 }
3480 }
3481 }
3482
3483 #[test]
3484 fn builtin_docs_print_a_call_signature_without_a_name() {
3485 for name in ["Number", "Number.@call"] {
3486 let docs = builtin_docs(name).expect("Number built-in");
3487 assert!(docs.contains(" (value?: "), "{docs}");
3488 assert!(!docs.contains("@call"), "{docs}");
3489 }
3490 }
3491
3492 #[test]
3493 fn builtin_docs_folds_constructor_into_array() {
3494 let docs = builtin_docs("Array").expect("Array built-in");
3495 assert!(docs.contains("interface Array<"));
3496 assert!(docs.contains("interface ArrayConstructor"));
3497 assert!(docs.contains("const Array: ArrayConstructor;"));
3498 }
3499
3500 #[test]
3501 fn builtin_docs_show_regex_whole_match_type() {
3502 for name in ["RegExpMatchArray", "RegExpExecArray"] {
3503 let docs = builtin_docs(name).expect("regex match array built-in");
3504 assert!(docs.contains(&format!(
3505 "interface {name} extends Array<string | undefined> {{\n \"0\": string;"
3506 )));
3507 }
3508 }
3509
3510 #[test]
3511 fn builtin_docs_renders_temporal_namespace() {
3512 let docs = builtin_docs("Temporal").expect("Temporal built-in");
3513 assert!(docs.starts_with("namespace Temporal {"));
3514 assert!(docs.contains("interface Instant"));
3515 assert!(docs.contains("namespace Now {"));
3516 assert!(docs.contains(" interface Instant"));
3518 assert_eq!(docs.matches("const Instant").count(), 1, "{docs}");
3521 }
3522
3523 #[test]
3524 fn builtin_docs_handles_json_intrinsic() {
3525 let docs = builtin_docs("JSON").expect("JSON built-in");
3526 assert!(docs.contains(
3527 "function stringify<T>(value: T, replacer?: null, space?: number | string | null): string;"
3528 ));
3529 assert!(docs.contains("function parse(text: string): unknown;"));
3530 }
3531
3532 #[test]
3533 fn builtin_docs_is_case_insensitive() {
3534 assert_eq!(builtin_docs("array"), builtin_docs("Array"));
3536 assert_eq!(builtin_docs("temporal"), builtin_docs("Temporal"));
3537 assert_eq!(builtin_docs("json"), builtin_docs("JSON"));
3538 assert_eq!(builtin_docs("ARRAY"), builtin_docs("Array"));
3540 }
3541
3542 #[test]
3543 fn builtin_docs_unknown_is_none() {
3544 assert!(builtin_docs("Promise").is_none());
3545 assert!(builtin_docs("Temporal#Instant").is_none());
3546 }
3547
3548 #[test]
3549 fn packages_d_ts_imports_the_types_a_package_borrows() {
3550 let mut package = PackageDeclaration::with_package("@pied-piper/files");
3551 let download = crate::stdlib::http::package_declaration()
3552 .values
3553 .get("download")
3554 .expect("submilli:http declares download")
3555 .clone();
3556 package.values.insert("fetch".to_string(), download);
3557
3558 let docs = render_packages_d_ts(&[&package], &[]);
3559
3560 assert!(docs.starts_with("declare module \"@pied-piper/files\" {\n"));
3561 assert!(docs.contains(" import type { DownloadResult } from \"submilli:http\";\n"));
3562 assert!(docs.contains(" import type { DownloadOptions } from \"submilli:http\";\n"));
3563 assert!(!docs.contains("import type { Response }"));
3564 }
3565
3566 #[test]
3567 fn a_borrowed_type_is_imported_from_its_own_module_when_others_share_its_name() {
3568 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3571 leaf.types
3572 .insert("Page".to_string(), alias_symbol("@pied-piper/leaf", "Page"));
3573 let mut other = PackageDeclaration::with_package("@pied-piper/other");
3574 other.types.insert(
3575 "Page".to_string(),
3576 alias_symbol("@pied-piper/other", "Page"),
3577 );
3578 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3579 let page = reference(
3580 "@pied-piper/leaf",
3581 crate::mangle::package_symbol("@pied-piper/leaf", "Page"),
3582 "Page",
3583 );
3584 mid.values.insert(
3585 "page".to_string(),
3586 const_symbol("@pied-piper/mid", "page", page),
3587 );
3588
3589 let docs = render_packages_d_ts(&[&leaf, &other, &mid], &[]);
3590
3591 assert!(docs.contains(" import type { Page } from \"@pied-piper/leaf\";\n"));
3592 assert!(!docs.contains("from \"submilli:session\""));
3593 assert!(!docs.contains("from \"@pied-piper/other\""));
3594 }
3595
3596 fn alias_symbol(package: &str, name: &str) -> crate::TypeSymbol {
3597 crate::TypeSymbol {
3598 name: name.to_string(),
3599 mangled_name: crate::mangle::package_symbol(package, name),
3600 declaration_span: crate::Span::at(crate::FileId(0)),
3601 kind: crate::TypeKind::Alias {
3602 generics: Default::default(),
3603 ty: Type::Number,
3604 doc: None,
3605 },
3606 }
3607 }
3608
3609 fn const_symbol(package: &str, name: &str, ty: Type) -> ValueSymbol {
3610 ValueSymbol {
3611 name: name.to_string(),
3612 mangled_name: crate::mangle::package_symbol(package, name),
3613 declaration_span: crate::Span::at(crate::FileId(0)),
3614 kind: ValueKind::Const { ty, doc: None },
3615 }
3616 }
3617
3618 fn reference(package: &str, mangled: crate::MangledName, local_name: &str) -> Type {
3619 Type::alias_ref(
3620 crate::types::Package(package.to_string()),
3621 local_name,
3622 mangled,
3623 Vec::new(),
3624 )
3625 }
3626
3627 #[test]
3628 fn an_aliased_type_is_imported_under_the_name_the_text_uses() {
3629 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3630 leaf.types
3631 .insert("Page".to_string(), alias_symbol("@pied-piper/leaf", "Page"));
3632 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3633 let page = reference(
3634 "@pied-piper/leaf",
3635 crate::mangle::package_symbol("@pied-piper/leaf", "Page"),
3636 "LeafPage",
3637 );
3638 mid.values.insert(
3639 "page".to_string(),
3640 const_symbol("@pied-piper/mid", "page", page),
3641 );
3642
3643 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3644
3645 assert!(docs.contains(" import type { Page as LeafPage } from \"@pied-piper/leaf\";\n"));
3646 assert!(!docs.contains("declare module \"@pied-piper/leaf\""));
3647 }
3648
3649 #[test]
3650 fn a_type_reexported_from_an_internal_module_is_imported_from_the_package() {
3651 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3652 leaf.types.insert(
3653 "Status".to_string(),
3654 alias_symbol("@pied-piper/leaf", "Status"),
3655 );
3656 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3657 let status = reference(
3658 "@pied-piper/leaf",
3659 crate::mangle::package_module_symbol("@pied-piper/leaf", "model", "Status"),
3660 "Status",
3661 );
3662 mid.values.insert(
3663 "status".to_string(),
3664 const_symbol("@pied-piper/mid", "status", status),
3665 );
3666
3667 let docs = render_packages_d_ts(&[&leaf, &mid], &[]);
3668
3669 assert!(docs.contains(" import type { Status } from \"@pied-piper/leaf\";\n"));
3670 }
3671
3672 #[test]
3673 fn only_the_rendered_declarations_bring_imports() {
3674 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3675 let response = reference(
3676 "submilli:http",
3677 crate::mangle::package_symbol("submilli:http", "Response"),
3678 "Response",
3679 );
3680 mid.runtime_globals.insert(
3681 crate::mangle::package_symbol("@pied-piper/mid", "hidden"),
3682 response.clone(),
3683 );
3684
3685 let docs = render_packages_d_ts(&[&mid], &[]);
3686
3687 assert!(!docs.contains("import type"), "{docs}");
3688 }
3689
3690 #[test]
3691 fn a_value_named_like_a_borrowed_interface_leaves_its_import() {
3692 let download = reference(
3695 "submilli:http",
3696 crate::mangle::package_symbol("submilli:http", "DownloadResult"),
3697 "DownloadResult",
3698 );
3699 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3700 mid.values.insert(
3701 "DownloadResult".to_string(),
3702 const_symbol("@pied-piper/mid", "DownloadResult", download),
3703 );
3704
3705 let docs = render_packages_d_ts(&[&mid], &[]);
3706
3707 assert!(
3708 docs.contains(" import type { DownloadResult } from \"submilli:http\";\n"),
3709 "{docs}"
3710 );
3711 }
3712
3713 #[test]
3714 fn type_arguments_bring_imports_and_alias_bodies_do_not() {
3715 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3716 leaf.types
3717 .insert("Page".to_string(), alias_symbol("@pied-piper/leaf", "Page"));
3718 leaf.types
3719 .insert("Item".to_string(), alias_symbol("@pied-piper/leaf", "Item"));
3720 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3721 let item = reference(
3722 "@pied-piper/leaf",
3723 crate::mangle::package_symbol("@pied-piper/leaf", "Item"),
3724 "Item",
3725 );
3726 let page_of_items = Type::alias_ref(
3727 crate::types::Package("@pied-piper/leaf".to_string()),
3728 "Page",
3729 crate::mangle::package_symbol("@pied-piper/leaf", "Page"),
3730 vec![Type::Array(Box::new(item))],
3731 );
3732 mid.values.insert(
3733 "page".to_string(),
3734 const_symbol("@pied-piper/mid", "page", page_of_items),
3735 );
3736 let response = reference(
3739 "submilli:http",
3740 crate::mangle::package_symbol("submilli:http", "Response"),
3741 "Response",
3742 );
3743 let wrapped = Type::Alias {
3744 mangled: crate::mangle::package_symbol("@pied-piper/leaf", "Item"),
3745 package: crate::types::Package("@pied-piper/leaf".to_string()),
3746 name: "Item".to_string(),
3747 args: Vec::new(),
3748 ty: Box::new(response),
3749 };
3750 mid.values.insert(
3751 "item".to_string(),
3752 const_symbol("@pied-piper/mid", "item", wrapped),
3753 );
3754
3755 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3756
3757 assert!(docs.contains(" import type { Page } from \"@pied-piper/leaf\";\n"));
3758 assert!(docs.contains(" import type { Item } from \"@pied-piper/leaf\";\n"));
3759 assert!(!docs.contains("Response"), "{docs}");
3760 }
3761
3762 fn class_symbol(
3763 package: &str,
3764 name: &str,
3765 extends: Option<crate::ClassExtends>,
3766 ) -> crate::TypeSymbol {
3767 crate::TypeSymbol {
3768 name: name.to_string(),
3769 mangled_name: crate::mangle::package_symbol(package, name),
3770 declaration_span: crate::Span::at(crate::FileId(0)),
3771 kind: TypeKind::Class {
3772 generics: Default::default(),
3773 fields: BTreeMap::new(),
3774 narrowing_checks: BTreeMap::new(),
3775 methods: BTreeMap::new(),
3776 method_visibility: BTreeMap::new(),
3777 accessors: Vec::new(),
3778 constructor: Vec::new(),
3779 constructor_visibility: crate::Visibility::Public,
3780 statics: BTreeMap::new(),
3781 static_visibility: BTreeMap::new(),
3782 static_fields: BTreeMap::new(),
3783 extends,
3784 implements: Vec::new(),
3785 doc: None,
3786 },
3787 }
3788 }
3789
3790 #[test]
3791 fn a_class_renders_and_imports_its_parent() {
3792 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3793 leaf.types.insert(
3794 "Base".to_string(),
3795 class_symbol("@pied-piper/leaf", "Base", None),
3796 );
3797 leaf.types
3798 .insert("Item".to_string(), alias_symbol("@pied-piper/leaf", "Item"));
3799 let item = reference(
3800 "@pied-piper/leaf",
3801 crate::mangle::package_symbol("@pied-piper/leaf", "Item"),
3802 "Item",
3803 );
3804 let extends = crate::ClassExtends {
3805 parent: crate::mangle::package_symbol("@pied-piper/leaf", "Base"),
3806 args: vec![item],
3807 };
3808 let mut sub = class_symbol("@pied-piper/mid", "Sub", Some(extends));
3809 if let TypeKind::Class { fields, .. } = &mut sub.kind {
3810 let response = reference(
3812 "submilli:http",
3813 crate::mangle::package_symbol("submilli:http", "Response"),
3814 "Response",
3815 );
3816 fields.insert(
3817 "hidden".to_string(),
3818 crate::FieldSig {
3819 ty: response,
3820 visibility: crate::Visibility::Private,
3821 readonly: false,
3822 optional: false,
3823 method: false,
3824 doc: None,
3825 },
3826 );
3827 }
3828 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
3829 mid.types.insert("Sub".to_string(), sub);
3830
3831 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3832
3833 assert!(
3834 docs.contains(" import type { Base } from \"@pied-piper/leaf\";\n"),
3835 "{docs}"
3836 );
3837 assert!(
3838 docs.contains(" import type { Item } from \"@pied-piper/leaf\";\n"),
3839 "{docs}"
3840 );
3841 assert!(
3842 docs.contains(" export class Sub extends Base<Item> {"),
3843 "{docs}"
3844 );
3845 assert!(!docs.contains("Response"), "{docs}");
3846 }
3847
3848 fn subclass_of(package: &str, name: &str, parent: crate::MangledName) -> crate::TypeSymbol {
3849 class_symbol(package, name, Some(crate::ClassExtends::plain(parent)))
3850 }
3851
3852 fn two_bases() -> (PackageDeclaration, PackageDeclaration) {
3854 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
3855 leaf.types.insert(
3856 "Base".to_string(),
3857 class_symbol("@pied-piper/leaf", "Base", None),
3858 );
3859 let mut other = PackageDeclaration::with_package("@pied-piper/other");
3860 other.types.insert(
3861 "Base".to_string(),
3862 class_symbol("@pied-piper/other", "Base", None),
3863 );
3864 (leaf, other)
3865 }
3866
3867 #[test]
3868 fn a_parent_named_like_a_local_value_is_imported_under_another_name() {
3869 let (leaf, _) = two_bases();
3870 let mut package = PackageDeclaration::with_package("@pied-piper/shadowed");
3871 package.values.insert(
3872 "Base".to_string(),
3873 const_symbol("@pied-piper/shadowed", "Base", Type::Number),
3874 );
3875 let base = crate::mangle::package_symbol("@pied-piper/leaf", "Base");
3876 package.types.insert(
3877 "Sub".to_string(),
3878 subclass_of("@pied-piper/shadowed", "Sub", base),
3879 );
3880
3881 let docs = render_packages_d_ts(&[&package], &[&leaf]);
3882
3883 assert!(
3884 docs.contains(" import type { Base as Base1 } from \"@pied-piper/leaf\";\n"),
3885 "{docs}"
3886 );
3887 assert!(
3888 docs.contains(" export class Sub extends Base1 {"),
3889 "{docs}"
3890 );
3891 }
3892
3893 #[test]
3894 fn parents_that_share_a_public_name_get_different_local_names() {
3895 let (leaf, other) = two_bases();
3896 let mut package = PackageDeclaration::with_package("@pied-piper/both");
3897 package.types.insert(
3898 "A".to_string(),
3899 subclass_of(
3900 "@pied-piper/both",
3901 "A",
3902 crate::mangle::package_symbol("@pied-piper/leaf", "Base"),
3903 ),
3904 );
3905 package.types.insert(
3906 "B".to_string(),
3907 subclass_of(
3908 "@pied-piper/both",
3909 "B",
3910 crate::mangle::package_symbol("@pied-piper/other", "Base"),
3911 ),
3912 );
3913
3914 let docs = render_packages_d_ts(&[&package], &[&leaf, &other]);
3915
3916 assert!(
3917 docs.contains(" import type { Base } from \"@pied-piper/leaf\";\n"),
3918 "{docs}"
3919 );
3920 assert!(
3921 docs.contains(" import type { Base as Base1 } from \"@pied-piper/other\";\n"),
3922 "{docs}"
3923 );
3924 assert!(docs.contains(" export class A extends Base {"), "{docs}");
3925 assert!(docs.contains(" export class B extends Base1 {"), "{docs}");
3926 }
3927
3928 #[test]
3929 fn a_parent_already_imported_under_an_alias_keeps_it() {
3930 let (leaf, _) = two_bases();
3931 let base = crate::mangle::package_symbol("@pied-piper/leaf", "Base");
3932 let mut package = PackageDeclaration::with_package("@pied-piper/aliased");
3933 package.values.insert(
3934 "base".to_string(),
3935 const_symbol(
3936 "@pied-piper/aliased",
3937 "base",
3938 reference("@pied-piper/leaf", base.clone(), "LB"),
3939 ),
3940 );
3941 package.types.insert(
3942 "Sub".to_string(),
3943 subclass_of("@pied-piper/aliased", "Sub", base),
3944 );
3945
3946 let docs = render_packages_d_ts(&[&package], &[&leaf]);
3947
3948 assert!(
3949 docs.contains(" import type { Base as LB } from \"@pied-piper/leaf\";\n"),
3950 "{docs}"
3951 );
3952 assert!(docs.contains(" export class Sub extends LB {"), "{docs}");
3953 assert!(!docs.contains("import type { Base }"), "{docs}");
3954 }
3955
3956 #[test]
3957 fn a_class_extending_a_builtin_error_renders_its_parent_as_the_global() {
3958 let error = builtin_package_declaration()
3959 .types
3960 .get("Error")
3961 .expect("the prelude declares Error")
3962 .mangled_name
3963 .clone();
3964 let mut package = PackageDeclaration::with_package("@pied-piper/errors");
3965 package.types.insert(
3966 "BillingError".to_string(),
3967 subclass_of("@pied-piper/errors", "BillingError", error),
3968 );
3969
3970 let docs = render_packages_d_ts(&[&package], &[]);
3971
3972 assert!(
3973 docs.contains(" export class BillingError extends Error {"),
3974 "{docs}"
3975 );
3976 assert!(!docs.contains("import type"), "{docs}");
3977 }
3978
3979 #[test]
3980 fn a_package_value_named_like_the_global_parent_leaves_the_class_without_it() {
3981 let error = builtin_package_declaration()
3982 .types
3983 .get("Error")
3984 .expect("the prelude declares Error")
3985 .mangled_name
3986 .clone();
3987 let mut package = PackageDeclaration::with_package("@pied-piper/errors");
3988 package.values.insert(
3989 "Error".to_string(),
3990 const_symbol("@pied-piper/errors", "Error", Type::Number),
3991 );
3992 package.types.insert(
3993 "BillingError".to_string(),
3994 subclass_of("@pied-piper/errors", "BillingError", error),
3995 );
3996
3997 let docs = render_packages_d_ts(&[&package], &[]);
3998
3999 assert!(docs.contains(" export class BillingError {"), "{docs}");
4000 }
4001
4002 #[test]
4003 fn global_functions_are_in_the_editor_declarations() {
4004 let lib = render_lib_submilli_d_ts();
4005 for function in [
4006 "parseInt(",
4007 "parseFloat(",
4008 "encodeURIComponent(",
4009 "encodeURI(",
4010 "decodeURIComponent(",
4011 "decodeURI(",
4012 "isNaN(",
4013 "isFinite(",
4014 ] {
4015 assert!(
4016 lib.contains(&format!("declare function {function}")),
4017 "{function}"
4018 );
4019 }
4020 }
4021
4022 #[test]
4023 fn builtin_errors_extend_error_in_the_editor_declarations() {
4024 let lib = render_lib_submilli_d_ts();
4025 for class in [
4026 "QuotaExceededError",
4027 "RangeError",
4028 "TypeError",
4029 "SyntaxError",
4030 "URIError",
4031 "ReferenceError",
4032 "PermissionDeniedError",
4033 "TimeoutError",
4034 "MissingValueError",
4035 ] {
4036 assert!(
4037 lib.contains(&format!("declare class {class} extends Error {{")),
4038 "{class}"
4039 );
4040 }
4041 assert!(lib.contains("declare class Error {"));
4042 }
4043
4044 #[test]
4045 fn a_value_named_like_a_borrowed_class_keeps_the_class_out() {
4046 let mut leaf = PackageDeclaration::with_package("@pied-piper/leaf");
4048 leaf.types.insert(
4049 "Base".to_string(),
4050 class_symbol("@pied-piper/leaf", "Base", None),
4051 );
4052 let base = Type::class_ref(
4053 crate::types::Package("@pied-piper/leaf".to_string()),
4054 "Base",
4055 crate::mangle::package_symbol("@pied-piper/leaf", "Base"),
4056 Vec::new(),
4057 );
4058 let mut mid = PackageDeclaration::with_package("@pied-piper/mid");
4059 mid.values.insert(
4060 "Base".to_string(),
4061 const_symbol("@pied-piper/mid", "Base", base),
4062 );
4063
4064 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
4065
4066 assert!(!docs.contains("import type"), "{docs}");
4067 }
4068
4069 #[test]
4070 fn a_package_type_named_like_a_constructor_is_an_ordinary_type() {
4071 let mut package = PackageDeclaration::with_package("@pied-piper/leaf");
4072 package.types.insert(
4073 "Client".to_string(),
4074 alias_symbol("@pied-piper/leaf", "Client"),
4075 );
4076 package.types.insert(
4077 "ClientConstructor".to_string(),
4078 alias_symbol("@pied-piper/leaf", "ClientConstructor"),
4079 );
4080
4081 let docs = render_packages_d_ts(&[&package], &[]);
4082
4083 assert!(docs.contains(" export type Client = number;"), "{docs}");
4084 assert!(
4085 docs.contains(" export type ClientConstructor = number;"),
4086 "{docs}"
4087 );
4088 assert!(!docs.contains("const Client"), "{docs}");
4089 }
4090
4091 #[test]
4092 fn stdlib_d_ts_declares_each_user_module() {
4093 let docs = every_package().render_stdlib_d_ts();
4094 for module in user_modules(every_package()) {
4095 assert!(
4096 docs.contains(&format!("declare module \"{}\" {{", module.package_name)),
4097 "missing declare module for {}",
4098 module.package_name
4099 );
4100 }
4101 assert!(docs.contains("declare module \"submilli:security\" {\n"));
4102 assert!(docs.contains(" export function check<T>("));
4103 assert!(docs.contains("declare module \"submilli:uuid\" {\n"));
4104 assert!(docs.contains(" export function v4("));
4105 assert!(docs.contains(" function delete_("));
4106 assert!(docs.contains(" export { delete_ as delete };"));
4107 assert!(!docs.contains("export function delete("));
4108 }
4109
4110 #[test]
4111 fn lib_submilli_d_ts_renders_ambient_builtins() {
4112 let docs = render_lib_submilli_d_ts();
4113 assert!(docs.contains("interface Array<"));
4114 for name in ["RegExpMatchArray", "RegExpExecArray"] {
4115 assert!(docs.contains(&format!(
4116 "interface {name} extends Array<string | undefined> {{\n \"0\": string;"
4117 )));
4118 }
4119 assert!(docs.contains("interface ArrayConstructor"));
4120 assert!(docs.contains("declare const Array: ArrayConstructor;"));
4121 assert_eq!(
4122 docs.matches("declare const Array: ArrayConstructor;")
4123 .count(),
4124 1
4125 );
4126 assert!(docs.contains("declare const console: Console;"));
4127 assert!(docs.contains("declare namespace Math {"));
4128 assert!(docs.contains("declare namespace Temporal {"));
4129 assert!(docs.contains("declare namespace JSON {"));
4130 assert!(docs.contains(
4131 "function stringify<T>(value: T, replacer?: null, space?: number | string | null): string;"
4132 ));
4133 assert!(docs.contains("interface Function {\n /** The parameters before the first optional, defaulted or rest one. */\n readonly length: number;\n}"));
4134 assert!(docs.contains("interface IArguments {}"));
4135 assert!(docs.contains("declare class Error {"));
4136 assert!(docs.contains("static isError(value?: unknown): value is Error;"));
4137 assert!(!docs.contains("ErrorConstructor"));
4138 assert!(!docs.contains("declare module"));
4139 assert!(!docs.contains("function string_concat"));
4140 }
4141
4142 #[test]
4143 fn d_ts_renderer_uses_typescript_call_and_predicate_signatures() {
4144 let docs = render_lib_submilli_d_ts();
4145 assert!(docs.contains("(value?: number | bigint | string | boolean): number;"));
4146 assert!(docs.contains("isArray<T>(value: T): value is T & unknown[];"));
4147 assert!(!docs.contains("@call"));
4148 }
4149
4150 fn class_with_constructor(
4151 package: &str,
4152 name: &str,
4153 constructor: Vec<Param>,
4154 constructor_visibility: crate::Visibility,
4155 extends: Option<ClassExtends>,
4156 ) -> TypeSymbol {
4157 TypeSymbol {
4158 name: name.to_string(),
4159 mangled_name: crate::mangle::package_symbol(package, name),
4160 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
4161 kind: TypeKind::Class {
4162 generics: Default::default(),
4163 fields: BTreeMap::new(),
4164 narrowing_checks: BTreeMap::new(),
4165 methods: BTreeMap::new(),
4166 method_visibility: BTreeMap::new(),
4167 accessors: Vec::new(),
4168 constructor,
4169 constructor_visibility,
4170 statics: BTreeMap::new(),
4171 static_visibility: BTreeMap::new(),
4172 static_fields: BTreeMap::new(),
4173 extends,
4174 implements: Vec::new(),
4175 doc: None,
4176 },
4177 }
4178 }
4179
4180 #[test]
4181 fn a_private_constructor_stays_out_of_the_docs_and_is_protected_in_d_ts() {
4182 let package = "@pied-piper/shapes";
4183 let mut defs = PackageDeclaration::with_package(package);
4184 let point = class_with_constructor(
4185 package,
4186 "Point",
4187 vec![Param::new("secret", Type::String)],
4188 crate::Visibility::Private,
4189 None,
4190 );
4191 let labeled = class_with_constructor(
4192 package,
4193 "Labeled",
4194 vec![Param::new("secret", Type::String)],
4195 crate::Visibility::Private,
4196 Some(ClassExtends {
4197 parent: point.mangled_name.clone(),
4198 args: Vec::new(),
4199 }),
4200 );
4201 defs.types.insert("Point".to_string(), point);
4202 defs.types.insert("Labeled".to_string(), labeled);
4203
4204 let docs = render_declarations(&defs);
4205 assert!(docs.contains("class Point"), "{docs}");
4206 assert!(!docs.contains("constructor"), "{docs}");
4207
4208 let d_ts = render_packages_d_ts(&[&defs], &[]);
4211 assert!(d_ts.contains("class Labeled extends Point"), "{d_ts}");
4212 assert_eq!(
4213 d_ts.matches("protected constructor();").count(),
4214 2,
4215 "{d_ts}"
4216 );
4217 assert!(!d_ts.contains("secret"), "{d_ts}");
4218 }
4219
4220 #[test]
4221 fn constructor_visibility_round_trips_and_a_public_one_is_not_written() {
4222 let package = "@pied-piper/shapes";
4223 for visibility in [crate::Visibility::Private, crate::Visibility::Public] {
4224 let symbol = class_with_constructor(package, "Point", Vec::new(), visibility, None);
4225 let json = serde_json::to_string(&symbol.kind).unwrap();
4226 assert_eq!(
4227 json.contains("constructor_visibility"),
4228 !visibility.is_public()
4229 );
4230 let TypeKind::Class {
4231 constructor_visibility,
4232 ..
4233 } = serde_json::from_str::<TypeKind>(&json).unwrap()
4234 else {
4235 panic!("not a class: {json}");
4236 };
4237 assert_eq!(constructor_visibility, visibility);
4238 }
4239 }
4240
4241 #[test]
4242 fn class_declarations_omit_private_members() {
4243 use crate::{FieldSig, MethodSig, Visibility};
4244
4245 let field = |ty, visibility| FieldSig {
4246 ty,
4247 visibility,
4248 readonly: false,
4249 optional: false,
4250 method: false,
4251 doc: None,
4252 };
4253 let method = |ret| MethodSig {
4254 optional: false,
4255 generics: Default::default(),
4256 params: Vec::new(),
4257 ret,
4258 predicate: None,
4259 doc: None,
4260 };
4261
4262 let mut fields = BTreeMap::new();
4263 fields.insert("name".to_string(), field(Type::String, Visibility::Public));
4264 fields.insert(
4265 "sound".to_string(),
4266 field(Type::String, Visibility::Private),
4267 );
4268 let mut methods = BTreeMap::new();
4269 methods.insert("speak".to_string(), method(Type::String));
4270 methods.insert("secret".to_string(), method(Type::Number));
4271 let mut method_visibility = BTreeMap::new();
4272 method_visibility.insert("speak".to_string(), Visibility::Public);
4273 method_visibility.insert("secret".to_string(), Visibility::Private);
4274
4275 let mut defs = PackageDeclaration::with_package("@pied-piper/zoo");
4276 defs.types.insert(
4277 "Animal".to_string(),
4278 TypeSymbol {
4279 name: "Animal".to_string(),
4280 mangled_name: crate::mangle::package_symbol("@pied-piper/zoo", "Animal"),
4281 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
4282 kind: TypeKind::Class {
4283 generics: Default::default(),
4284 fields,
4285 narrowing_checks: BTreeMap::new(),
4286 methods,
4287 method_visibility,
4288 accessors: Vec::new(),
4289 constructor: vec![Param::new("name", Type::String)],
4290 constructor_visibility: crate::Visibility::Public,
4291 statics: BTreeMap::new(),
4292 static_visibility: BTreeMap::new(),
4293 static_fields: BTreeMap::new(),
4294 extends: None,
4295 implements: Vec::new(),
4296 doc: None,
4297 },
4298 },
4299 );
4300
4301 let rendered = render_declarations(&defs);
4302 assert!(rendered.contains("class Animal {"), "{rendered}");
4304 assert!(rendered.contains("name: string;"), "{rendered}");
4305 assert!(rendered.contains("speak(): string;"), "{rendered}");
4306 assert!(
4307 rendered.contains("constructor(name: string);"),
4308 "{rendered}"
4309 );
4310 assert!(!rendered.contains("private "), "{rendered}");
4312 assert!(!rendered.contains("sound"), "{rendered}");
4313 assert!(!rendered.contains("secret"), "{rendered}");
4314 }
4315
4316 #[test]
4317 fn class_declarations_render_accessors_faithfully() {
4318 use crate::{AccessorSig, FieldSig, Param, Visibility};
4319
4320 let prop = |ty: Type, readonly| FieldSig {
4323 ty,
4324 visibility: Visibility::Public,
4325 readonly,
4326 optional: false,
4327 method: false,
4328 doc: None,
4329 };
4330 let mut fields = BTreeMap::new();
4331 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 {
4337 name: name.to_string(),
4338 param: Param::new("v", ty),
4339 };
4340 let getter = |name: &str, ty: Type| AccessorSig::Getter {
4341 name: name.to_string(),
4342 ret_ty: ty,
4343 };
4344 let accessors = vec![
4345 getter("area", Type::Number),
4346 getter("size", Type::Number),
4347 setter("size", Type::Number),
4348 getter("label", Type::String),
4349 setter("label", Type::Number),
4350 setter("secret", Type::Number),
4351 ];
4352
4353 let mut defs = PackageDeclaration::with_package("@pied-piper/geo");
4354 defs.types.insert(
4355 "Shape".to_string(),
4356 TypeSymbol {
4357 name: "Shape".to_string(),
4358 mangled_name: crate::mangle::package_symbol("@pied-piper/geo", "Shape"),
4359 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
4360 kind: TypeKind::Class {
4361 generics: Default::default(),
4362 fields,
4363 narrowing_checks: BTreeMap::new(),
4364 methods: BTreeMap::new(),
4365 method_visibility: BTreeMap::new(),
4366 accessors,
4367 constructor: Vec::new(),
4368 constructor_visibility: crate::Visibility::Public,
4369 statics: BTreeMap::new(),
4370 static_visibility: BTreeMap::new(),
4371 static_fields: BTreeMap::new(),
4372 extends: None,
4373 implements: Vec::new(),
4374 doc: None,
4375 },
4376 },
4377 );
4378
4379 let docs = render_declarations(&defs);
4380 let shape = &defs.types.get("Shape").unwrap().kind;
4381 let mut dts = String::new();
4382 render_ts_type(&mut dts, "Shape", shape, "", "export ", None);
4383 for rendered in [&docs, &dts] {
4384 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}");
4388 assert!(rendered.contains("set secret(v: number);"), "{rendered}"); assert!(!rendered.contains("get area"), "{rendered}");
4391 assert!(!rendered.contains("get size("), "{rendered}");
4392 }
4393 }
4394
4395 #[test]
4396 fn class_declarations_render_public_statics_only() {
4397 use crate::{FieldSig, MethodSig, Param, Visibility};
4398
4399 let method = |ret: Type| MethodSig {
4400 optional: false,
4401 generics: Default::default(),
4402 params: vec![Param::new("x", Type::Number)],
4403 ret,
4404 predicate: None,
4405 doc: None,
4406 };
4407 let mut statics = BTreeMap::new();
4408 statics.insert("make".to_string(), method(Type::Number));
4409 statics.insert("hidden".to_string(), method(Type::Number));
4410 let mut static_visibility = BTreeMap::new();
4411 static_visibility.insert("make".to_string(), Visibility::Public);
4412 static_visibility.insert("hidden".to_string(), Visibility::Private);
4413 let mut static_fields = BTreeMap::new();
4414 static_fields.insert(
4415 "MAX".to_string(),
4416 FieldSig {
4417 ty: Type::Number,
4418 visibility: Visibility::Public,
4419 readonly: true,
4420 optional: false,
4421 method: false,
4422 doc: None,
4423 },
4424 );
4425 static_fields.insert(
4426 "KEY".to_string(),
4427 FieldSig {
4428 ty: Type::Number,
4429 visibility: Visibility::Private,
4430 readonly: true,
4431 optional: false,
4432 method: false,
4433 doc: None,
4434 },
4435 );
4436 static_fields.insert(
4437 "count".to_string(),
4438 FieldSig {
4439 ty: Type::Number,
4440 visibility: Visibility::Public,
4441 readonly: false,
4442 optional: false,
4443 method: false,
4444 doc: None,
4445 },
4446 );
4447 static_fields.insert(
4448 "seed".to_string(),
4449 FieldSig {
4450 ty: Type::Number,
4451 visibility: Visibility::Private,
4452 readonly: false,
4453 optional: false,
4454 method: false,
4455 doc: None,
4456 },
4457 );
4458
4459 let mut defs = PackageDeclaration::with_package("@pied-piper/calc");
4460 defs.types.insert(
4461 "Calc".to_string(),
4462 TypeSymbol {
4463 name: "Calc".to_string(),
4464 mangled_name: crate::mangle::package_symbol("@pied-piper/calc", "Calc"),
4465 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
4466 kind: TypeKind::Class {
4467 generics: Default::default(),
4468 fields: BTreeMap::new(),
4469 narrowing_checks: BTreeMap::new(),
4470 methods: BTreeMap::new(),
4471 method_visibility: BTreeMap::new(),
4472 accessors: Vec::new(),
4473 constructor: Vec::new(),
4474 constructor_visibility: crate::Visibility::Public,
4475 statics,
4476 static_visibility,
4477 static_fields,
4478 extends: None,
4479 implements: Vec::new(),
4480 doc: None,
4481 },
4482 },
4483 );
4484
4485 let docs = render_declarations(&defs);
4486 let calc = &defs.types.get("Calc").unwrap().kind;
4487 let mut dts = String::new();
4488 render_ts_type(&mut dts, "Calc", calc, "", "export ", None);
4489 for rendered in [&docs, &dts] {
4490 assert!(
4491 rendered.contains("static make(x: number): number;"),
4492 "{rendered}"
4493 );
4494 assert!(
4495 rendered.contains("static readonly MAX: number;"),
4496 "{rendered}"
4497 );
4498 assert!(rendered.contains("static count: number;"), "{rendered}");
4499 assert!(!rendered.contains("hidden"), "{rendered}");
4500 assert!(!rendered.contains("KEY"), "{rendered}");
4501 assert!(!rendered.contains("seed"), "{rendered}");
4502 }
4503 }
4504
4505 #[test]
4506 fn a_type_re_exported_under_another_name_renders_by_that_name() {
4507 let model = r#"
4508 export interface Res { ok: boolean; }
4509 export type Alias = string | Res;
4510 export class Thing { name: string = "t"; }
4511 export interface Both { n: number; }
4512 "#;
4513 let lib = r#"
4514 export { Res as Result, Alias as Either, Thing as Item, Both, Both as Pair } from "./model";
4515 import { Res, Alias, Thing, Both } from "./model";
4516 /** Get one. @returns the result */
4517 export function read(): Res { return { ok: true }; }
4518 /** Use one. @param a the value */
4519 export function either(a: Alias): void { }
4520 /** Make one. @param r a result @returns the thing */
4521 export function make(r: Res): Thing { return new Thing(); }
4522 /** A pair. @returns it */
4523 export function pair(): Both { return { n: 1 }; }
4524 /** A list of either. */
4525 export type Wrapped = Res[] | Thing;
4526 "#;
4527 let defs = crate::compile_package(
4528 "@pied-piper/renamed",
4529 crate::ModulePath::from("lib"),
4530 &[
4531 crate::PackageSourceModule {
4532 path: crate::ModulePath::from("lib"),
4533 source: lib,
4534 },
4535 crate::PackageSourceModule {
4536 path: crate::ModulePath::from("model"),
4537 source: model,
4538 },
4539 ],
4540 &[],
4541 )
4542 .unwrap_or_else(|diagnostics| panic!("compile failed: {diagnostics:#?}"))
4543 .declaration;
4544
4545 let docs = render_declarations(&defs);
4546 let d_ts = render_packages_d_ts(&[&defs], &[]);
4547 for rendered in [&docs, &d_ts] {
4548 assert!(rendered.contains("read(): Result;"), "{rendered}");
4549 assert!(rendered.contains("either(a: Either): void;"), "{rendered}");
4550 assert!(rendered.contains("make(r: Result): Item;"), "{rendered}");
4551 assert!(
4552 rendered.contains("type Either = string | Result;"),
4553 "{rendered}"
4554 );
4555 assert!(
4556 rendered.contains("type Wrapped = Result[] | Item;"),
4557 "{rendered}"
4558 );
4559 assert!(rendered.contains("pair(): Both;"), "{rendered}");
4561 for source_name in ["Res", "Alias", "Thing"] {
4562 assert!(
4563 !rendered.contains(&format!("{source_name};"))
4564 && !rendered.contains(&format!("{source_name}[]"))
4565 && !rendered.contains(&format!("{source_name})")),
4566 "{source_name} in {rendered}"
4567 );
4568 }
4569 }
4570 }
4571}