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