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" => "Session-scoped key-value state: get/has/set/remove/list.",
907 "submilli:skills" => "Harness skills: list(), load(name) and readFile(name, path).",
908 "submilli:url" => "URL parse/build and query-string handling. Pure compute.",
909 "submilli:uuid" => "UUID v4/v7 generation and validation.",
910 _ => "",
911 }
912}
913
914fn matches_query(defs: &PackageDeclaration, q: &str) -> bool {
915 if q.is_empty() {
916 return true;
917 }
918 let in_symbols = defs
919 .values
920 .keys()
921 .chain(defs.types.keys())
922 .any(|k| k.to_lowercase().contains(q));
923 defs.package_name.to_lowercase().contains(q)
924 || module_description(&defs.package_name)
925 .to_lowercase()
926 .contains(q)
927 || in_symbols
928}
929
930pub fn render_declarations(defs: &PackageDeclaration) -> String {
934 let mut out = String::new();
935 for (name, sym) in &defs.values {
936 push_doc(&mut out, value_doc(&sym.kind), "");
937 render_value(&mut out, name, &sym.kind, "");
938 }
939 for (name, sym) in &defs.types {
940 push_doc(&mut out, type_doc(&sym.kind), "");
941 render_type(&mut out, name, &sym.kind, "");
942 }
943 out.trim_end().to_string()
944}
945
946pub fn render_stdlib_d_ts() -> String {
953 Stdlib::core().render_stdlib_d_ts()
954}
955
956pub fn render_packages_d_ts(
966 packages: &[&PackageDeclaration],
967 context: &[&PackageDeclaration],
968) -> String {
969 Stdlib::core().render_packages_d_ts(packages, context)
970}
971
972fn global_classes() -> BTreeMap<String, String> {
975 let defs = builtin_package_declaration();
976 defs.types
977 .into_iter()
978 .filter(|(name, symbol)| {
979 matches!(symbol.kind, TypeKind::Class { .. })
980 && !is_hidden_prelude_type(&defs.package_name, name)
981 })
982 .map(|(name, symbol)| (symbol.mangled_name.as_str().to_string(), name))
983 .collect()
984}
985
986fn editor_stdlib_modules(stdlib: Stdlib) -> Vec<PackageDeclaration> {
987 let mut modules = user_modules(stdlib);
988 modules.push(crate::stdlib::test::package_declaration());
989 modules.push(crate::stdlib::security::package_declaration());
990 modules.sort_by(|a, b| a.package_name.cmp(&b.package_name));
991 modules
992}
993
994struct TypeExporters<'a> {
996 by_mangled: BTreeMap<&'a str, (ExportedType<'a>, &'a str)>,
998 by_module: BTreeMap<&'a str, BTreeMap<&'a str, ExportedType<'a>>>,
1001}
1002
1003#[derive(Clone, Copy)]
1004struct ExportedType<'a> {
1005 name: &'a str,
1006 is_value: bool,
1009}
1010
1011impl<'a> TypeExporters<'a> {
1012 fn new(modules: impl Iterator<Item = &'a PackageDeclaration>) -> Self {
1013 let mut by_mangled = BTreeMap::new();
1014 let mut by_module: BTreeMap<&str, BTreeMap<&str, ExportedType>> = BTreeMap::new();
1015 for defs in modules {
1016 let module = defs.package_name.as_str();
1017 for (name, symbol) in &defs.types {
1018 let exported = ExportedType {
1019 name: name.as_str(),
1020 is_value: matches!(
1021 symbol.kind,
1022 TypeKind::Class { .. }
1023 | TypeKind::NumberEnum { .. }
1024 | TypeKind::StringEnum { .. }
1025 ),
1026 };
1027 by_mangled.insert(symbol.mangled_name.as_str(), (exported, module));
1028 by_module
1029 .entry(module)
1030 .or_default()
1031 .insert(name.as_str(), exported);
1032 }
1033 }
1034 Self {
1035 by_mangled,
1036 by_module,
1037 }
1038 }
1039
1040 fn resolve(&self, reference: &TypeReference) -> Option<(ExportedType<'a>, &'a str)> {
1043 if let Some(&exported) = self.by_mangled.get(reference.mangled.as_str()) {
1044 return Some(exported);
1045 }
1046 let (module, types) = self.by_module.get_key_value(reference.package.as_str())?;
1047 Some((*types.get(reference.name.as_str())?, module))
1048 }
1049}
1050
1051struct TypeReference {
1055 mangled: String,
1056 package: String,
1057 name: String,
1058}
1059
1060struct BlockImports<'a> {
1062 types: BTreeMap<String, (&'a str, &'a str)>,
1065 parents: BTreeMap<&'a str, String>,
1068}
1069
1070fn block_imports<'a>(
1083 defs: &'a PackageDeclaration,
1084 exporters: &TypeExporters<'a>,
1085) -> BlockImports<'a> {
1086 let mut types = BTreeMap::new();
1087 for reference in rendered_type_references(defs) {
1088 let Some((exported, module)) = exporters.resolve(&reference) else {
1089 continue;
1090 };
1091 if module != defs.package_name && !declares(defs, &reference.name, exported.is_value) {
1092 types
1093 .entry(reference.name)
1094 .or_insert((exported.name, module));
1095 }
1096 }
1097 let mut parents = BTreeMap::new();
1098 for (mangled, exported, module) in class_parents(defs, exporters) {
1099 if module == defs.package_name {
1100 parents.insert(mangled, exported.name.to_string());
1101 continue;
1102 }
1103 let already_imported = types
1104 .iter()
1105 .find(|(_, target)| **target == (exported.name, module))
1106 .map(|(local, _)| local.clone());
1107 let local = if let Some(local) = already_imported {
1108 local
1109 } else {
1110 let local = free_local_name(defs, &types, exported.name);
1111 types.insert(local.clone(), (exported.name, module));
1112 local
1113 };
1114 parents.insert(mangled, local);
1115 }
1116 BlockImports { types, parents }
1117}
1118
1119fn free_local_name(
1123 defs: &PackageDeclaration,
1124 imports: &BTreeMap<String, (&str, &str)>,
1125 public: &str,
1126) -> String {
1127 let is_free = |name: &str| !imports.contains_key(name) && !declares(defs, name, true);
1128 if is_free(public) {
1129 return public.to_string();
1130 }
1131 let taken = imports
1132 .len()
1133 .saturating_add(defs.types.len())
1134 .saturating_add(defs.values.len())
1135 .saturating_add(defs.namespaces.len());
1136 (1..=taken.saturating_add(1))
1137 .map(|suffix| format!("{public}{suffix}"))
1138 .find(|name| is_free(name))
1139 .unwrap_or_else(|| format!("{public}{}", taken.saturating_add(1)))
1140}
1141
1142fn declares(defs: &PackageDeclaration, name: &str, borrowed_is_value: bool) -> bool {
1145 defs.types.contains_key(name)
1146 || (borrowed_is_value
1147 && (defs.values.contains_key(name) || defs.namespaces.contains_key(name)))
1148}
1149
1150fn class_parents<'a>(
1153 defs: &'a PackageDeclaration,
1154 exporters: &TypeExporters<'a>,
1155) -> Vec<(&'a str, ExportedType<'a>, &'a str)> {
1156 defs.types
1157 .values()
1158 .filter_map(|symbol| match &symbol.kind {
1159 TypeKind::Class {
1160 extends: Some(extends),
1161 ..
1162 } => {
1163 let parent = extends.parent.as_str();
1164 let &(exported, module) = exporters.by_mangled.get(parent)?;
1165 Some((parent, exported, module))
1166 }
1167 _ => None,
1168 })
1169 .collect()
1170}
1171
1172fn rendered_type_references(defs: &PackageDeclaration) -> Vec<TypeReference> {
1184 let types: BTreeMap<&String, TypeSymbol> = defs
1185 .types
1186 .iter()
1187 .map(|(name, symbol)| (name, public_surface(symbol)))
1188 .collect();
1189 let rendered = [
1190 serde_json::to_value(&defs.values),
1191 serde_json::to_value(&types),
1192 serde_json::to_value(&defs.namespaces),
1193 ];
1194 let mut pending: Vec<&serde_json::Value> = rendered.iter().flatten().collect();
1196 let mut references = Vec::new();
1197 while let Some(value) = pending.pop() {
1198 match value {
1199 serde_json::Value::Object(fields) => {
1200 let Some(serde_json::Value::String(mangled)) = fields.get("mangled") else {
1201 pending.extend(fields.values());
1202 continue;
1203 };
1204 let text = |key: &str| {
1205 fields
1206 .get(key)
1207 .and_then(serde_json::Value::as_str)
1208 .unwrap_or_default()
1209 .to_string()
1210 };
1211 references.push(TypeReference {
1212 mangled: mangled.clone(),
1213 package: text("package"),
1214 name: text("name"),
1215 });
1216 pending.extend(fields.get("args"));
1217 }
1218 serde_json::Value::Array(items) => pending.extend(items),
1219 _ => {}
1220 }
1221 }
1222 references
1223}
1224
1225fn public_surface(symbol: &TypeSymbol) -> TypeSymbol {
1228 let mut symbol = symbol.clone();
1229 if let TypeKind::Class {
1230 fields,
1231 narrowing_checks,
1232 methods,
1233 method_visibility,
1234 accessors,
1235 constructor,
1236 constructor_visibility,
1237 statics,
1238 static_visibility,
1239 static_fields,
1240 ..
1241 } = &mut symbol.kind
1242 {
1243 if *constructor_visibility == crate::Visibility::Private {
1244 constructor.clear();
1245 }
1246 let is_public = |visibility: Option<&crate::Visibility>| {
1247 visibility != Some(&crate::Visibility::Private)
1248 };
1249 accessors.retain(|accessor| is_public(fields.get(accessor.name()).map(|f| &f.visibility)));
1250 fields.retain(|_, field| is_public(Some(&field.visibility)));
1251 static_fields.retain(|_, field| is_public(Some(&field.visibility)));
1252 methods.retain(|name, _| is_public(method_visibility.get(name)));
1253 statics.retain(|name, _| is_public(static_visibility.get(name)));
1254 narrowing_checks.clear();
1255 }
1256 symbol
1257}
1258
1259pub fn render_lib_submilli_d_ts() -> String {
1262 let defs = builtin_package_declaration();
1263 let globals = global_classes();
1264 let parent = |extends: &ClassExtends| {
1265 let global = globals.get(extends.parent.as_str())?;
1266 Some(ts_named_type(global, &extends.args))
1267 };
1268 let mut out = String::new();
1269 render_ts_declarations(&mut out, &defs, "", "declare ", &parent);
1270 render_ts_compiler_globals(&mut out, &defs);
1271 let json = json_package_declaration();
1272 render_ts_declarations(&mut out, &json, "", "declare ", &|_| None);
1273 out.trim_end().to_string()
1274}
1275
1276fn render_ts_compiler_globals(out: &mut String, defs: &PackageDeclaration) {
1277 if !defs.types.contains_key("Function") {
1278 out.push_str("interface Function {}\n\n");
1279 }
1280 out.push_str("interface CallableFunction extends Function {}\n\n");
1281 out.push_str("interface NewableFunction extends Function {}\n\n");
1282 out.push_str("interface IArguments {}\n\n");
1283 out.push_str(
1284 "/** Throw `Error(message)` if `condition` is false. Compiler intrinsic — always in scope, not imported. */\n\
1285 declare function assert(condition: boolean, message?: string): void;\n\n",
1286 );
1287 render_ts_checker_plumbing(out, defs);
1288}
1289
1290fn render_ts_checker_plumbing(out: &mut String, defs: &PackageDeclaration) {
1298 out.push_str("declare const Symbol: { readonly iterator: unique symbol };\n\n");
1299 out.push_str(
1300 "interface Array<T> {\n [index: number]: T;\n [Symbol.iterator](): Iterator<T>;\n}\n\n",
1301 );
1302 for (name, sym) in &defs.types {
1303 if let TypeKind::Interface {
1304 generics, methods, ..
1305 } = &sym.kind
1306 && let Some(m) = methods.get("iterator")
1307 {
1308 let _ = writeln!(
1309 out,
1310 "interface {name}{} {{\n [Symbol.iterator](): {};\n}}\n",
1311 ts_interface_generics(name, generics, "declare "),
1312 ts_type(&m.ret)
1313 );
1314 }
1315 }
1316}
1317
1318fn render_declare_module(out: &mut String, defs: &PackageDeclaration) {
1319 let _ = writeln!(out, "declare module \"{}\" {{", defs.package_name);
1320 render_ts_declarations(out, defs, " ", "export ", &|_| None);
1321 let _ = writeln!(out, "}}\n");
1322}
1323
1324fn render_ts_declarations(
1327 out: &mut String,
1328 defs: &PackageDeclaration,
1329 indent: &str,
1330 export_prefix: &str,
1331 class_parent: &dyn Fn(&ClassExtends) -> Option<String>,
1332) {
1333 for (name, sym) in &defs.values {
1334 if is_hidden_prelude_value(&defs.package_name, name) {
1335 continue;
1336 }
1337 push_doc(out, value_doc(&sym.kind), indent);
1338 render_ts_value(out, name, &sym.kind, indent, export_prefix);
1339 }
1340 let pairs_constructors = defs.package_name.starts_with("submilli:");
1343 for (name, sym) in &defs.types {
1344 if is_hidden_prelude_type(&defs.package_name, name) {
1345 continue;
1346 }
1347 if pairs_constructors && name.ends_with("Constructor") {
1348 continue;
1349 }
1350 if pairs_constructors && defs.types.contains_key(&format!("{name}Constructor")) {
1351 render_ts_type_with_ctor(
1352 out,
1353 &defs.types,
1354 name,
1355 indent,
1356 export_prefix,
1357 !defs.values.contains_key(name),
1358 );
1359 } else {
1360 push_doc(out, type_doc(&sym.kind), indent);
1361 let parent = match &sym.kind {
1362 TypeKind::Class {
1363 extends: Some(extends),
1364 ..
1365 } => class_parent(extends),
1366 _ => None,
1367 };
1368 render_ts_type(
1369 out,
1370 name,
1371 &sym.kind,
1372 indent,
1373 export_prefix,
1374 parent.as_deref(),
1375 );
1376 }
1377 }
1378 for (name, ns) in &defs.namespaces {
1379 render_ts_namespace(out, name, ns, indent, export_prefix);
1380 }
1381}
1382
1383fn is_hidden_prelude_value(package_name: &str, name: &str) -> bool {
1384 package_name == crate::mangle::PRELUDE_PACKAGE
1385 && (name.starts_with("string_")
1386 || matches!(
1387 name,
1388 "string_concat" | "string_eq" | "string_length" | "string_cmp"
1389 ))
1390}
1391
1392fn is_hidden_prelude_type(package_name: &str, name: &str) -> bool {
1393 package_name == crate::mangle::PRELUDE_PACKAGE && name.contains('#')
1394}
1395
1396fn render_ts_value(
1397 out: &mut String,
1398 name: &str,
1399 kind: &ValueKind,
1400 indent: &str,
1401 export_prefix: &str,
1402) {
1403 let reserved_export = export_prefix == "export " && is_ts_reserved_word(name);
1404 let declared_name = if reserved_export {
1405 format!("{name}_")
1406 } else {
1407 name.to_string()
1408 };
1409 let decl_export_prefix = if reserved_export { "" } else { export_prefix };
1410 match kind {
1411 ValueKind::Function {
1412 generics,
1413 params,
1414 ret,
1415 type_predicate,
1416 ..
1417 } => {
1418 let ret = ts_return_type(
1419 params,
1420 ret,
1421 type_predicate.as_ref(),
1422 value_doc(kind).as_ref(),
1423 );
1424 let _ = writeln!(
1425 out,
1426 "{indent}{decl_export_prefix}function {declared_name}{}({}): {ret};",
1427 generics_str(generics),
1428 ts_params_str(params, value_doc(kind).as_ref())
1429 );
1430 }
1431 ValueKind::Let { ty, .. } => {
1432 let _ = writeln!(
1433 out,
1434 "{indent}{decl_export_prefix}let {declared_name}: {};",
1435 ts_type(ty)
1436 );
1437 }
1438 ValueKind::Const { ty, .. } => {
1439 let _ = writeln!(
1440 out,
1441 "{indent}{decl_export_prefix}const {declared_name}: {};",
1442 ts_type(ty)
1443 );
1444 }
1445 }
1446 if reserved_export {
1447 let _ = writeln!(out, "{indent}export {{ {declared_name} as {name} }};");
1448 }
1449 out.push('\n');
1450}
1451
1452fn render_ts_type(
1454 out: &mut String,
1455 name: &str,
1456 kind: &TypeKind,
1457 indent: &str,
1458 export_prefix: &str,
1459 parent: Option<&str>,
1460) {
1461 let inner = format!("{indent} ");
1462 match kind {
1463 TypeKind::Interface {
1464 generics,
1465 methods,
1466 properties,
1467 index,
1468 ..
1469 } => {
1470 let _ = writeln!(
1471 out,
1472 "{indent}{export_prefix}interface {name}{} {{",
1473 ts_interface_generics(name, generics, export_prefix)
1474 );
1475 if let Some(index) = index {
1476 let ro = if index.readonly { "readonly " } else { "" };
1477 let _ = writeln!(out, "{inner}{ro}[key: string]: {};", ts_type(&index.value));
1478 }
1479 for (pname, prop) in properties {
1480 push_doc(out, &prop.doc, &inner);
1481 let ro = if prop.readonly { "readonly " } else { "" };
1482 let opt = if prop.optional { "?" } else { "" };
1483 let pname = member_name(pname);
1484 let _ = writeln!(out, "{inner}{ro}{pname}{opt}: {};", ts_type(&prop.ty));
1485 }
1486 for (mname, m) in methods {
1487 push_doc(out, &m.doc, &inner);
1488 let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1489 let optional = if m.optional { "?" } else { "" };
1490 let mname = method_prefix(mname);
1491 let _ = writeln!(
1492 out,
1493 "{inner}{mname}{optional}{}({}): {ret};",
1494 generics_str(&m.generics),
1495 ts_params_str(&m.params, m.doc.as_ref())
1496 );
1497 }
1498 let _ = writeln!(out, "{indent}}}\n");
1499 }
1500 TypeKind::NumberEnum { variants, .. } => {
1501 let _ = writeln!(out, "{indent}{export_prefix}enum {name} {{");
1502 for (v, value) in variants {
1503 let _ = writeln!(out, "{inner}{v} = {value},");
1504 }
1505 let _ = writeln!(out, "{indent}}}\n");
1506 }
1507 TypeKind::StringEnum { variants, .. } => {
1508 let _ = writeln!(out, "{indent}{export_prefix}enum {name} {{");
1509 for (v, value) in variants {
1510 let _ = writeln!(out, "{inner}{v} = \"{}\",", escape_string_literal(value));
1511 }
1512 let _ = writeln!(out, "{indent}}}\n");
1513 }
1514 TypeKind::Alias { generics, ty, .. } => {
1515 let _ = writeln!(
1516 out,
1517 "{indent}{export_prefix}type {name}{} = {};\n",
1518 generics_str(generics),
1519 ts_type(ty)
1520 );
1521 }
1522 TypeKind::Class {
1523 generics,
1524 fields,
1525 methods,
1526 method_visibility,
1527 statics,
1528 static_visibility,
1529 static_fields,
1530 accessors,
1531 constructor,
1532 constructor_visibility,
1533 ..
1534 } => {
1535 let extends = parent.map(|parent| format!(" extends {parent}"));
1540 let _ = writeln!(
1541 out,
1542 "{indent}{export_prefix}class {name}{}{} {{",
1543 generics_str(generics),
1544 extends.unwrap_or_default()
1545 );
1546 for (fname, field) in static_fields {
1547 if field.visibility == crate::Visibility::Private {
1548 continue;
1549 }
1550 push_doc(out, &field.doc, &inner);
1551 let ro = if field.readonly { "readonly " } else { "" };
1552 let fname = member_name(fname);
1553 let _ = writeln!(out, "{inner}static {ro}{fname}: {};", ts_type(&field.ty));
1554 }
1555 for (mname, m) in statics {
1556 if static_visibility.get(mname) == Some(&crate::Visibility::Private) {
1557 continue;
1558 }
1559 push_doc(out, &m.doc, &inner);
1560 let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1561 let mname = member_name(mname);
1562 let _ = writeln!(
1563 out,
1564 "{inner}static {mname}{}({}): {ret};",
1565 generics_str(&m.generics),
1566 ts_params_str(&m.params, m.doc.as_ref())
1567 );
1568 }
1569 for (fname, field) in fields {
1570 if field.visibility == crate::Visibility::Private
1571 || accessors.iter().any(|a| a.name() == fname)
1572 {
1573 continue;
1574 }
1575 push_doc(out, &field.doc, &inner);
1576 let ro = if field.readonly { "readonly " } else { "" };
1577 let opt = if field.optional { "?" } else { "" };
1578 let fname = member_name(fname);
1579 let _ = writeln!(out, "{inner}{ro}{fname}{opt}: {};", ts_type(&field.ty));
1580 }
1581 render_class_accessors(out, fields, accessors, &inner, ts_type);
1582 if *constructor_visibility == crate::Visibility::Private {
1588 let _ = writeln!(out, "{inner}protected constructor();");
1589 } else {
1590 let _ = writeln!(
1591 out,
1592 "{inner}constructor({});",
1593 ts_params_str(constructor, None)
1594 );
1595 }
1596 for (mname, m) in methods {
1597 if method_visibility.get(mname) == Some(&crate::Visibility::Private) {
1598 continue;
1599 }
1600 push_doc(out, &m.doc, &inner);
1601 let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1602 let optional = if m.optional { "?" } else { "" };
1603 let mname = member_name(mname);
1604 let _ = writeln!(
1605 out,
1606 "{inner}{mname}{optional}{}({}): {ret};",
1607 generics_str(&m.generics),
1608 ts_params_str(&m.params, m.doc.as_ref())
1609 );
1610 }
1611 let _ = writeln!(out, "{indent}}}\n");
1612 }
1613 }
1614}
1615
1616fn render_class_accessors(
1621 out: &mut String,
1622 fields: &std::collections::BTreeMap<String, crate::FieldSig>,
1623 accessors: &[crate::AccessorSig],
1624 inner: &str,
1625 fmt_ty: impl Fn(&Type) -> String,
1626) {
1627 use crate::AccessorSig;
1628 let mut by_name: std::collections::BTreeMap<&str, (Option<&Type>, Option<&crate::Param>)> =
1629 std::collections::BTreeMap::new();
1630 for acc in accessors {
1631 let entry = by_name.entry(acc.name()).or_insert((None, None));
1632 match acc {
1633 AccessorSig::Getter { ret_ty, .. } => entry.0 = Some(ret_ty),
1634 AccessorSig::Setter { param, .. } => entry.1 = Some(param),
1635 }
1636 }
1637 for (name, (get, set)) in by_name {
1638 if fields.get(name).map(|f| f.visibility) == Some(crate::Visibility::Private) {
1639 continue;
1640 }
1641 let name = member_name(name);
1642 match (get, set) {
1643 (Some(r), Some(p)) if r == &p.ty => {
1644 let _ = writeln!(out, "{inner}{name}: {};", fmt_ty(r));
1645 }
1646 (Some(r), Some(p)) => {
1647 let _ = writeln!(out, "{inner}get {name}(): {};", fmt_ty(r));
1648 let _ = writeln!(out, "{inner}set {name}({}: {});", p.name, fmt_ty(&p.ty));
1649 }
1650 (Some(r), None) => {
1651 let _ = writeln!(out, "{inner}readonly {name}: {};", fmt_ty(r));
1652 }
1653 (None, Some(p)) => {
1654 let _ = writeln!(out, "{inner}set {name}({}: {});", p.name, fmt_ty(&p.ty));
1655 }
1656 (None, None) => {}
1657 }
1658 }
1659}
1660
1661fn render_ts_type_with_ctor(
1662 out: &mut String,
1663 types: &BTreeMap<String, TypeSymbol>,
1664 name: &str,
1665 indent: &str,
1666 export_prefix: &str,
1667 synthesize_binding: bool,
1668) {
1669 if let Some(sym) = types.get(name) {
1670 push_doc(out, type_doc(&sym.kind), indent);
1671 render_ts_type(out, name, &sym.kind, indent, export_prefix, None);
1672 }
1673 let ctor = format!("{name}Constructor");
1674 if let Some(sym) = types.get(&ctor) {
1675 push_doc(out, type_doc(&sym.kind), indent);
1676 render_ts_type(out, &ctor, &sym.kind, indent, export_prefix, None);
1677 if synthesize_binding {
1678 let _ = writeln!(out, "{indent}{export_prefix}const {name}: {ctor};\n");
1679 }
1680 }
1681}
1682
1683fn render_ts_namespace(
1684 out: &mut String,
1685 name: &str,
1686 ns: &NamespaceSymbol,
1687 indent: &str,
1688 export_prefix: &str,
1689) {
1690 push_doc(out, &ns.doc, indent);
1691 let _ = writeln!(out, "{indent}{export_prefix}namespace {name} {{");
1692 let inner = format!("{indent} ");
1693 for (vname, sym) in &ns.values {
1694 push_doc(out, value_doc(&sym.kind), &inner);
1695 render_ts_value(out, vname, &sym.kind, &inner, "");
1696 }
1697 for (tname, sym) in &ns.types {
1698 push_doc(out, type_doc(&sym.kind), &inner);
1699 render_ts_type(out, tname, &sym.kind, &inner, "", None);
1700 }
1701 for (nname, sub) in &ns.namespaces {
1702 render_ts_namespace(out, nname, sub, &inner, "");
1703 }
1704 let _ = writeln!(out, "{indent}}}\n");
1705}
1706
1707fn ts_params_str(params: &[Param], doc: Option<&DocComment>) -> String {
1708 rendered_params(params, doc, ts_type)
1709}
1710
1711fn ts_return_type(
1712 params: &[Param],
1713 ret: &Type,
1714 predicate: Option<&TypePredicate>,
1715 doc: Option<&DocComment>,
1716) -> String {
1717 let Some(predicate) = predicate else {
1718 return ts_type(ret);
1719 };
1720 let Some(param) = params.get(predicate.parameter_index as usize) else {
1721 return ts_type(ret);
1722 };
1723 let names = parameter_display_names(params, doc);
1724 let Some(name) = names.get(predicate.parameter_index as usize) else {
1725 return ts_type(ret);
1726 };
1727 let asserted = ts_type(&predicate.asserted_type);
1728 match param.ty {
1729 Type::TypeVar(_) | Type::GenericParam { .. } => {
1730 format!("{name} is {} & {asserted}", ts_type(¶m.ty))
1731 }
1732 _ => format!("{name} is {asserted}"),
1733 }
1734}
1735
1736fn ts_type(ty: &Type) -> String {
1737 match ty {
1738 Type::Refined { original, ty } => format!("({} & {})", ts_type(original), ts_type(ty)),
1739 Type::Number => "number".to_string(),
1740 Type::NumberLiteral(n) => n.0.to_string(),
1741 Type::BigInt => "bigint".to_string(),
1742 Type::BigIntLiteral(digits) => format!("{digits}n"),
1743 Type::String => "string".to_string(),
1744 Type::StringLiteral(s) => format!("\"{}\"", escape_string_literal(s)),
1745 Type::Uint8Array => "Uint8Array".to_string(),
1746 Type::Boolean => "boolean".to_string(),
1747 Type::BooleanLiteral(value) => value.to_string(),
1748 Type::Null => "null".to_string(),
1749 Type::Undefined => "undefined".to_string(),
1750 Type::Void => "void".to_string(),
1751 Type::Unknown => "unknown".to_string(),
1752 Type::Function {
1753 params,
1754 ret,
1755 predicate,
1756 has_rest,
1757 optional,
1758 } => {
1759 let params = params
1760 .iter()
1761 .enumerate()
1762 .map(|(i, p)| {
1763 let prefix = if *has_rest && i == params.len().saturating_sub(1) {
1764 "..."
1765 } else {
1766 ""
1767 };
1768 let fixed = params.len().saturating_sub(usize::from(*has_rest));
1769 if i < fixed && i >= fixed.saturating_sub(*optional) {
1770 let shown = crate::types::shown_beside_optional_marker(p);
1771 format!("{prefix}arg{i}?: {}", ts_type(&shown))
1772 } else {
1773 format!("{prefix}arg{i}: {}", ts_type(p))
1774 }
1775 })
1776 .collect::<Vec<_>>()
1777 .join(", ");
1778 let ret = predicate.as_deref().map_or_else(
1779 || ts_type(ret),
1780 |p| format!("arg{} is {}", p.parameter_index, ts_type(&p.asserted_type)),
1781 );
1782 format!("({params}) => {ret}")
1783 }
1784 Type::Object { fields, index } => {
1785 let mut members: Vec<String> = fields
1786 .iter()
1787 .map(|(name, field)| {
1788 let opt = if field.optional { "?" } else { "" };
1789 let ro = if field.readonly { "readonly " } else { "" };
1790 let name = member_name(name);
1791 format!("{ro}{name}{opt}: {}", ts_type(&field.ty))
1792 })
1793 .collect();
1794 if let Some(index) = index {
1795 let ro = if index.readonly { "readonly " } else { "" };
1796 members.push(format!("{ro}[key: string]: {}", ts_type(&index.value)));
1797 }
1798 if members.is_empty() {
1799 "{}".into()
1800 } else {
1801 format!("{{ {} }}", members.join("; "))
1802 }
1803 }
1804 Type::Array(elem) => format!("{}[]", ts_type_array_element(elem)),
1805 Type::Tuple(elements) => {
1806 let elements = elements
1807 .iter()
1808 .enumerate()
1809 .map(|(index, ty)| {
1810 if index >= elements.len().saturating_sub(elements.optional) {
1811 format!("({})?", ts_type(ty))
1812 } else {
1813 ts_type(ty)
1814 }
1815 })
1816 .collect::<Vec<_>>()
1817 .join(", ");
1818 format!("[{elements}]")
1819 }
1820 Type::Readonly(inner) => format!("readonly {}", ts_type(inner)),
1821 Type::Error => "never".to_string(),
1822 Type::Never => "never".to_string(),
1823 Type::TypeVar(name) | Type::GenericParam { name, .. } => name.clone(),
1824 Type::InterfaceRef { name, args, .. }
1825 | Type::ClassRef { name, args, .. }
1826 | Type::Alias { name, args, .. } => ts_named_type(name, args),
1827 Type::NumberEnum { name, .. } | Type::StringEnum { name, .. } => name.clone(),
1828 Type::Union(members) => crate::types::union_display_order(members)
1829 .into_iter()
1830 .map(ts_type_union_member)
1831 .collect::<Vec<_>>()
1832 .join(" | "),
1833 Type::AliasRef { name, args, .. } => ts_named_type(name, args),
1834 }
1835}
1836
1837fn ts_named_type(name: &str, args: &[Type]) -> String {
1838 if args.is_empty() {
1839 return name.to_string();
1840 }
1841 let args = args.iter().map(ts_type).collect::<Vec<_>>().join(", ");
1842 format!("{name}<{args}>")
1843}
1844
1845fn ts_type_array_element(ty: &Type) -> String {
1846 match ty {
1847 Type::Function { .. } | Type::Union(_) | Type::Readonly(_) => format!("({})", ts_type(ty)),
1848 _ => ts_type(ty),
1849 }
1850}
1851
1852fn ts_type_union_member(ty: &Type) -> String {
1853 match ty {
1854 Type::Function { .. } => format!("({})", ts_type(ty)),
1855 _ => ts_type(ty),
1856 }
1857}
1858
1859fn is_ts_reserved_word(name: &str) -> bool {
1860 matches!(
1861 name,
1862 "break"
1863 | "case"
1864 | "catch"
1865 | "class"
1866 | "const"
1867 | "continue"
1868 | "debugger"
1869 | "default"
1870 | "delete"
1871 | "do"
1872 | "else"
1873 | "enum"
1874 | "export"
1875 | "extends"
1876 | "false"
1877 | "finally"
1878 | "for"
1879 | "function"
1880 | "if"
1881 | "import"
1882 | "in"
1883 | "instanceof"
1884 | "new"
1885 | "null"
1886 | "return"
1887 | "super"
1888 | "switch"
1889 | "this"
1890 | "throw"
1891 | "true"
1892 | "try"
1893 | "typeof"
1894 | "var"
1895 | "void"
1896 | "while"
1897 | "with"
1898 | "as"
1899 | "async"
1900 | "await"
1901 | "from"
1902 | "get"
1903 | "let"
1904 | "of"
1905 | "set"
1906 | "static"
1907 | "using"
1908 | "yield"
1909 )
1910}
1911
1912fn json_package_declaration() -> PackageDeclaration {
1913 let mut defs = PackageDeclaration::with_package(crate::mangle::PRELUDE_PACKAGE);
1914 let json_prefix = crate::mangle::prelude(JSON_BUILTIN);
1915 let mut json = NamespaceSymbol {
1916 name: JSON_BUILTIN.to_string(),
1917 mangled_prefix: json_prefix.clone(),
1918 declaration_span: Span::at(FileId::JSON),
1919 values: BTreeMap::new(),
1920 types: BTreeMap::new(),
1921 namespaces: BTreeMap::new(),
1922 doc: None,
1923 };
1924 json.values.insert(
1925 "stringify".to_string(),
1926 ValueSymbol {
1927 name: "stringify".to_string(),
1928 mangled_name: crate::mangle::extend(&json_prefix, "stringify"),
1929 declaration_span: Span::at(FileId::JSON),
1930 kind: ValueKind::Function {
1931 generics: vec!["T".to_string()],
1932 params: vec![
1933 Param::new("value", Type::TypeVar("T".to_string())),
1934 Param {
1935 name: "replacer".to_string(),
1936 ty: Type::union(vec![Type::Null, Type::Undefined]),
1937 default: Some(crate::DefaultValue::Undefined),
1938 optional: false,
1939 rest: false,
1940 },
1941 Param {
1942 name: "space".to_string(),
1943 ty: Type::union(vec![Type::Number, Type::String, Type::Null, Type::Undefined]),
1944 default: Some(crate::DefaultValue::Undefined),
1945 optional: false,
1946 rest: false,
1947 },
1948 ],
1949 ret: Type::union(vec![Type::String, Type::Undefined]),
1950 type_predicate: None,
1951 doc: crate::doc(FileId::JSON, "/** Serialize a value to a JSON string, or return undefined when it has no JSON representation. */"),
1952 },
1953 },
1954 );
1955 json.values.insert(
1956 "parse".to_string(),
1957 ValueSymbol {
1958 name: "parse".to_string(),
1959 mangled_name: crate::mangle::extend(&json_prefix, "parse"),
1960 declaration_span: Span::at(FileId::JSON),
1961 kind: ValueKind::Function {
1962 generics: Vec::new(),
1963 params: vec![Param::new("text", Type::String)],
1964 ret: Type::Unknown,
1965 type_predicate: None,
1966 doc: crate::doc(
1967 FileId::JSON,
1968 "/** Parse a JSON string as unknown; use `JSON.parse(s) as T` to validate a target type. */",
1969 ),
1970 },
1971 },
1972 );
1973 defs.namespaces.insert(JSON_BUILTIN.to_string(), json);
1974 defs
1975}
1976
1977fn render_value(out: &mut String, name: &str, kind: &ValueKind, indent: &str) {
1978 match kind {
1979 ValueKind::Function {
1980 generics,
1981 params,
1982 ret,
1983 ..
1984 } => {
1985 let _ = writeln!(
1986 out,
1987 "{indent}function {name}{}({}): {ret};\n",
1988 generics_str(generics),
1989 params_str(params, value_doc(kind).as_ref())
1990 );
1991 }
1992 ValueKind::Let { ty, .. } => {
1993 let _ = writeln!(out, "{indent}let {name}: {ty};\n");
1994 }
1995 ValueKind::Const { ty, .. } => {
1996 let _ = writeln!(out, "{indent}const {name}: {ty};\n");
1997 }
1998 }
1999}
2000
2001fn render_type(out: &mut String, name: &str, kind: &TypeKind, indent: &str) {
2002 let inner = format!("{indent} ");
2003 match kind {
2004 TypeKind::Interface {
2005 generics,
2006 methods,
2007 properties,
2008 ..
2009 } => {
2010 let _ = writeln!(out, "{indent}interface {name}{} {{", generics_str(generics));
2011 for (pname, prop) in properties {
2012 push_doc(out, &prop.doc, &inner);
2013 let ro = if prop.readonly { "readonly " } else { "" };
2014 let opt = if prop.optional { "?" } else { "" };
2015 let pname = member_name(pname);
2016 let _ = writeln!(out, "{inner}{ro}{pname}{opt}: {};", prop.ty);
2017 }
2018 for (mname, m) in methods {
2019 push_doc(out, &m.doc, &inner);
2020 let mname = method_prefix(mname);
2021 let _ = writeln!(
2022 out,
2023 "{inner}{mname}{}{}({}): {};",
2024 if m.optional { "?" } else { "" },
2025 generics_str(&m.generics),
2026 params_str(&m.params, m.doc.as_ref()),
2027 m.ret
2028 );
2029 }
2030 let _ = writeln!(out, "{indent}}}\n");
2031 }
2032 TypeKind::NumberEnum { variants, .. } => {
2033 let _ = writeln!(out, "{indent}enum {name} {{");
2034 for (v, value) in variants {
2035 let _ = writeln!(out, "{inner}{v} = {value},");
2036 }
2037 let _ = writeln!(out, "{indent}}}\n");
2038 }
2039 TypeKind::StringEnum { variants, .. } => {
2040 let _ = writeln!(out, "{indent}enum {name} {{");
2041 for (v, value) in variants {
2042 let _ = writeln!(out, "{inner}{v} = \"{}\",", escape_string_literal(value));
2043 }
2044 let _ = writeln!(out, "{indent}}}\n");
2045 }
2046 TypeKind::Alias { generics, ty, .. } => {
2047 let _ = writeln!(
2048 out,
2049 "{indent}type {name}{} = {ty};\n",
2050 generics_str(generics)
2051 );
2052 }
2053 TypeKind::Class {
2054 generics,
2055 fields,
2056 methods,
2057 method_visibility,
2058 statics,
2059 static_visibility,
2060 static_fields,
2061 accessors,
2062 constructor,
2063 constructor_visibility,
2064 ..
2065 } => {
2066 let _ = writeln!(out, "{indent}class {name}{} {{", generics_str(generics));
2071 for (fname, field) in static_fields {
2072 if field.visibility == crate::Visibility::Private {
2073 continue;
2074 }
2075 push_doc(out, &field.doc, &inner);
2076 let ro = if field.readonly { "readonly " } else { "" };
2077 let fname = member_name(fname);
2078 let _ = writeln!(out, "{inner}static {ro}{fname}: {};", field.ty);
2079 }
2080 for (mname, m) in statics {
2081 if static_visibility.get(mname) == Some(&crate::Visibility::Private) {
2082 continue;
2083 }
2084 push_doc(out, &m.doc, &inner);
2085 let mname = member_name(mname);
2086 let _ = writeln!(
2087 out,
2088 "{inner}static {mname}{}({}): {};",
2089 generics_str(&m.generics),
2090 params_str(&m.params, m.doc.as_ref()),
2091 m.ret
2092 );
2093 }
2094 for (fname, field) in fields {
2095 if field.visibility == crate::Visibility::Private
2096 || accessors.iter().any(|a| a.name() == fname)
2097 {
2098 continue;
2099 }
2100 push_doc(out, &field.doc, &inner);
2101 let ro = if field.readonly { "readonly " } else { "" };
2102 let opt = if field.optional { "?" } else { "" };
2103 let fname = member_name(fname);
2104 let _ = writeln!(out, "{inner}{ro}{fname}{opt}: {};", field.ty);
2105 }
2106 render_class_accessors(out, fields, accessors, &inner, ToString::to_string);
2107 if *constructor_visibility != crate::Visibility::Private {
2108 let _ = writeln!(
2109 out,
2110 "{inner}constructor({});",
2111 params_str(constructor, None)
2112 );
2113 }
2114 for (mname, m) in methods {
2115 if method_visibility.get(mname) == Some(&crate::Visibility::Private) {
2116 continue;
2117 }
2118 push_doc(out, &m.doc, &inner);
2119 let mname = member_name(mname);
2120 let _ = writeln!(
2121 out,
2122 "{inner}{mname}{}{}({}): {};",
2123 if m.optional { "?" } else { "" },
2124 generics_str(&m.generics),
2125 params_str(&m.params, m.doc.as_ref()),
2126 m.ret
2127 );
2128 }
2129 let _ = writeln!(out, "{indent}}}\n");
2130 }
2131 }
2132}
2133
2134fn render_type_with_ctor(
2139 out: &mut String,
2140 types: &BTreeMap<String, TypeSymbol>,
2141 name: &str,
2142 indent: &str,
2143) {
2144 if let Some(sym) = types.get(name) {
2145 push_doc(out, type_doc(&sym.kind), indent);
2146 render_type(out, name, &sym.kind, indent);
2147 }
2148 let ctor = format!("{name}Constructor");
2149 if let Some(sym) = types.get(&ctor) {
2150 push_doc(out, type_doc(&sym.kind), indent);
2151 render_type(out, &ctor, &sym.kind, indent);
2152 let _ = writeln!(out, "{indent}const {name}: {ctor};\n");
2153 }
2154}
2155
2156fn render_namespace(out: &mut String, name: &str, ns: &NamespaceSymbol, indent: &str) {
2157 let _ = writeln!(out, "{indent}namespace {name} {{");
2158 let inner = format!("{indent} ");
2159 for (vname, sym) in &ns.values {
2160 push_doc(out, value_doc(&sym.kind), &inner);
2161 render_value(out, vname, &sym.kind, &inner);
2162 }
2163 for (tname, sym) in &ns.types {
2164 push_doc(out, type_doc(&sym.kind), &inner);
2167 render_type(out, tname, &sym.kind, &inner);
2168 }
2169 for (nname, sub) in &ns.namespaces {
2170 render_namespace(out, nname, sub, &inner);
2171 }
2172 let _ = writeln!(out, "{indent}}}\n");
2173}
2174
2175fn generics_str(generics: &[String]) -> String {
2176 if generics.is_empty() {
2177 String::new()
2178 } else {
2179 format!("<{}>", generics.join(", "))
2180 }
2181}
2182
2183fn ts_interface_generics(name: &str, generics: &[String], export_prefix: &str) -> String {
2187 if export_prefix == "declare "
2188 && matches!(name, "Iterable" | "Iterator")
2189 && let [t] = generics
2190 {
2191 return format!("<{t}, TReturn = any, TNext = any>");
2192 }
2193 generics_str(generics)
2194}
2195
2196fn params_str(params: &[Param], doc: Option<&DocComment>) -> String {
2197 rendered_params(params, doc, ToString::to_string)
2198}
2199
2200fn rendered_params(
2201 params: &[Param],
2202 doc: Option<&DocComment>,
2203 format_type: impl Fn(&Type) -> String,
2204) -> String {
2205 let names = parameter_display_names(params, doc);
2206 params
2207 .iter()
2208 .zip(names)
2209 .enumerate()
2210 .map(|(index, (p, name))| {
2211 let prefix = if p.rest { "..." } else { "" };
2212 let later_required = params
2214 .iter()
2215 .skip(index.saturating_add(1))
2216 .any(|later| !later.is_omittable() && !later.rest);
2217 let opt = if p.is_omittable() && !later_required {
2218 "?"
2219 } else {
2220 ""
2221 };
2222 let ty = if opt.is_empty() {
2224 p.ty.clone()
2225 } else {
2226 crate::types::shown_beside_optional_marker(&p.ty)
2227 };
2228 format!("{prefix}{name}{opt}: {}", format_type(&ty))
2229 })
2230 .collect::<Vec<_>>()
2231 .join(", ")
2232}
2233
2234fn parameter_display_names(params: &[Param], doc: Option<&DocComment>) -> Vec<String> {
2237 let tags: Vec<_> = doc
2238 .into_iter()
2239 .flat_map(|doc| &doc.params)
2240 .filter(|tag| !tag.name.contains('.'))
2241 .collect();
2242 let mut used: BTreeSet<String> = params
2243 .iter()
2244 .filter(|p| !crate::lower_patterns::is_pattern_param(&p.name))
2245 .map(|p| p.name.clone())
2246 .collect();
2247 params
2248 .iter()
2249 .enumerate()
2250 .map(|(index, param)| {
2251 if !crate::lower_patterns::is_pattern_param(¶m.name) {
2252 return param.name.clone();
2253 }
2254 if let Some(tag) = tags.get(index)
2255 && valid_parameter_name(&tag.name)
2256 && used.insert(tag.name.clone())
2257 {
2258 return tag.name.clone();
2259 }
2260 let mut name = format!("arg{index}");
2261 while !used.insert(name.clone()) {
2262 name.push('_');
2263 }
2264 name
2265 })
2266 .collect()
2267}
2268
2269fn valid_parameter_name(name: &str) -> bool {
2270 let mut chars = name.chars();
2271 chars
2272 .next()
2273 .is_some_and(|c| c.is_ascii_alphabetic() || matches!(c, '_' | '$'))
2274 && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '$'))
2275 && !is_ts_reserved_word(name)
2276 && !matches!(
2277 name,
2278 "arguments"
2279 | "eval"
2280 | "implements"
2281 | "interface"
2282 | "package"
2283 | "private"
2284 | "protected"
2285 | "public"
2286 )
2287}
2288
2289fn push_doc(out: &mut String, doc: &Option<DocComment>, indent: &str) {
2290 let Some(doc) = doc else { return };
2291 let mut lines: Vec<String> = doc.summary.lines().map(str::to_string).collect();
2292 for p in &doc.params {
2293 lines.push(format!("@param {} {}", p.name, p.description));
2294 }
2295 for cap in &doc.capabilities {
2296 let mut line = format!("@capability {}", cap.capability);
2297 if !cap.bindings.is_empty() {
2298 let bindings = cap
2299 .bindings
2300 .iter()
2301 .map(|b| {
2302 let value = match &b.kind {
2303 DocCapabilityBindingKind::Parameter { param, path, .. }
2304 if *param == b.field && path.is_empty() =>
2305 {
2306 String::new()
2307 }
2308 DocCapabilityBindingKind::Parameter { param, path, .. } => {
2309 let suffix = path
2310 .iter()
2311 .map(|part| format!(".{part}"))
2312 .collect::<String>();
2313 format!(": ${param}{suffix}")
2314 }
2315 DocCapabilityBindingKind::Type { name, .. } => format!(": {name}"),
2316 DocCapabilityBindingKind::Literal { value, .. } => {
2317 format!(": {}", literal_str(value))
2318 }
2319 };
2320 format!("{}{}", b.field, value)
2321 })
2322 .collect::<Vec<_>>()
2323 .join(", ");
2324 line.push_str(&format!(" {{ {bindings} }}"));
2325 }
2326 if !cap.description.is_empty() {
2327 line.push_str(" - ");
2328 line.push_str(&cap.description);
2329 }
2330 lines.push(line);
2331 }
2332 if let Some(r) = &doc.returns {
2333 lines.push(format!("@returns {}", r.description));
2334 }
2335 if lines.is_empty() {
2336 return;
2337 }
2338 let _ = writeln!(out, "{indent}/**");
2339 for line in lines {
2340 let _ = writeln!(out, "{indent} * {line}");
2341 }
2342 let _ = writeln!(out, "{indent} */");
2343}
2344
2345fn literal_str(value: &DocCapabilityLiteral) -> String {
2346 match value {
2347 DocCapabilityLiteral::String(s) => {
2348 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
2349 }
2350 DocCapabilityLiteral::Number(n) => n.clone(),
2351 DocCapabilityLiteral::Boolean(v) => v.to_string(),
2352 DocCapabilityLiteral::Null => "null".to_string(),
2353 }
2354}
2355
2356fn value_doc(kind: &ValueKind) -> &Option<DocComment> {
2357 match kind {
2358 ValueKind::Function { doc, .. }
2359 | ValueKind::Let { doc, .. }
2360 | ValueKind::Const { doc, .. } => doc,
2361 }
2362}
2363
2364fn type_doc(kind: &TypeKind) -> &Option<DocComment> {
2365 match kind {
2366 TypeKind::Interface { doc, .. }
2367 | TypeKind::Class { doc, .. }
2368 | TypeKind::NumberEnum { doc, .. }
2369 | TypeKind::StringEnum { doc, .. }
2370 | TypeKind::Alias { doc, .. } => doc,
2371 }
2372}
2373
2374fn method_prefix(name: &str) -> Cow<'_, str> {
2378 match name {
2379 "@call" => Cow::Borrowed(""),
2380 "new" => Cow::Borrowed("new "),
2381 _ => member_name(name),
2382 }
2383}
2384
2385fn member_name(name: &str) -> Cow<'_, str> {
2390 if name.is_ascii() && crate::type_rendering::is_identifier_name(name) {
2391 return Cow::Borrowed(name);
2392 }
2393 Cow::Owned(format!("\"{}\"", escape_string_literal(name)))
2394}
2395
2396#[cfg(test)]
2397mod tests {
2398 use super::*;
2399
2400 #[test]
2401 fn member_names_are_quoted_unless_identifiers() {
2402 let mut lone = String::new();
2403 crate::literal_units::push_lone_surrogate(&mut lone, 0xD800);
2404 assert_eq!(member_name("count"), "count");
2405 assert_eq!(member_name("$el_2"), "$el_2");
2406 assert_eq!(member_name("café"), "\"café\"");
2407 assert_eq!(member_name("a b"), "\"a b\"");
2408 assert_eq!(member_name("0"), "\"0\"");
2409 assert_eq!(member_name(&lone), "\"\\uD800\"");
2410 }
2411
2412 #[test]
2413 fn destructured_display_names_are_valid_and_distinct() {
2414 let params = [
2415 Param::new("#pattern_p_0", Type::Number),
2416 Param::new("arg0", Type::Number),
2417 Param::new("", Type::Number),
2418 Param::new("#pattern_p_3", Type::Number),
2419 ];
2420 let doc = crate::doc(
2421 FileId(0),
2422 "/** Names.\n * @param class Reserved.\n * @param arg0 Existing.\n * @param arg0.x Property.\n * @param pair Pair.\n * @param pair Duplicate.\n */",
2423 );
2424 assert_eq!(
2425 parameter_display_names(¶ms, doc.as_ref()),
2426 ["arg0_", "arg0", "pair", "arg3"]
2427 );
2428 assert_eq!(
2429 params_str(¶ms, doc.as_ref()),
2430 "arg0_: number, arg0: number, pair: number, arg3: number"
2431 );
2432 assert_eq!(
2433 ts_params_str(¶ms, doc.as_ref()),
2434 "arg0_: number, arg0: number, pair: number, arg3: number"
2435 );
2436 }
2437
2438 #[test]
2439 fn catalog_filters_before_counting_omitted_entries() {
2440 let extra = (0..CATALOG_LIMIT)
2441 .map(|index| CatalogEntry {
2442 name: format!("@acme/package{index}"),
2443 source: "registry".into(),
2444 description: String::new(),
2445 })
2446 .collect();
2447 let catalog = catalog_filtered(extra, |name| !name.starts_with("submilli:"));
2448 assert_eq!(catalog.entries.len(), CATALOG_LIMIT);
2449 assert_eq!(catalog.remaining, 0);
2450 assert!(
2451 catalog
2452 .entries
2453 .iter()
2454 .all(|entry| entry.name.starts_with("@acme/"))
2455 );
2456 }
2457
2458 #[test]
2459 fn http_docs_render_signatures() {
2460 let doc = docs("submilli:http").expect("http module");
2461 assert_eq!(doc.name, "submilli:http");
2462 assert!(!doc.description.is_empty());
2463 assert!(doc.declarations.contains("function get("));
2464 assert!(doc.declarations.contains("function post("));
2465 assert!(doc.declarations.contains("@capability http.get"));
2466 assert!(doc.declarations.contains("@capability http.download"));
2467 }
2468
2469 #[test]
2470 fn fs_and_secrets_docs_render_capabilities() {
2471 let fs = docs("submilli:fs").expect("fs module");
2472 assert!(fs.declarations.contains("@capability fs.read"));
2473 assert!(fs.declarations.contains("@capability fs.write"));
2474 assert!(fs.declarations.contains("@capability fs.list"));
2475
2476 let secrets = docs("submilli:secrets").expect("secrets module");
2477 assert!(secrets.declarations.contains("@capability secrets.get"));
2478 }
2479
2480 #[test]
2481 fn security_is_not_user_facing() {
2482 assert!(docs("submilli:security").is_none());
2483 assert!(!search("").iter().any(|m| m.name == INTERNAL_MODULE));
2484 }
2485
2486 #[test]
2487 fn search_matches_symbol_names() {
2488 let hits = search("sha256");
2489 assert!(
2490 hits.iter().any(|m| m.name == "submilli:crypto"),
2491 "{:?}",
2492 names(&hits)
2493 );
2494 }
2495
2496 #[test]
2497 fn empty_query_lists_all_user_modules() {
2498 let names = names(&search(""));
2499 assert!(names.contains(&"submilli:crypto".to_string()));
2500 assert!(names.contains(&"submilli:http".to_string()));
2501 assert!(!names.contains(&INTERNAL_MODULE.to_string()));
2502 }
2503
2504 fn names(hits: &[ModuleSummary]) -> Vec<String> {
2505 hits.iter().map(|m| m.name.clone()).collect()
2506 }
2507
2508 #[test]
2509 fn builtins_lists_headline_types_and_namespaces() {
2510 let b = builtins();
2511 for expected in ["Array", "Map", "Set", "String", "Number", "Error"] {
2512 assert!(
2513 b.types.contains(&expected.to_string()),
2514 "missing {expected}"
2515 );
2516 }
2517 for hidden in [
2519 "ArrayConstructor",
2520 "Console",
2521 "Iterator",
2522 "Temporal#Instant",
2523 ] {
2524 assert!(!b.types.contains(&hidden.to_string()), "leaked {hidden}");
2525 }
2526 for ns in ["Math", "Temporal", "JSON"] {
2527 assert!(
2528 b.namespaces.contains(&ns.to_string()),
2529 "missing namespace {ns}"
2530 );
2531 }
2532 }
2533
2534 #[test]
2535 fn every_builtin_has_renderable_docs() {
2536 let b = builtins();
2537 for name in b.types.iter().chain(b.namespaces.iter()) {
2538 assert!(
2539 builtin_docs(name).is_some_and(|d| !d.is_empty()),
2540 "no docs for built-in {name}"
2541 );
2542 }
2543 }
2544
2545 #[test]
2546 fn dotted_path_returns_the_member_slice() {
2547 let slice = builtin_docs("Temporal.Instant").expect("Temporal.Instant");
2548 assert!(slice.contains("const Instant: Temporal.InstantConstructor;"));
2551 assert!(slice.contains("interface Instant {"));
2552 assert!(slice.contains("interface InstantConstructor {"));
2553 assert!(slice.starts_with("namespace Temporal {"), "{slice}");
2555 assert!(slice.contains("A point in time, to nanosecond precision"));
2557 assert!(!slice.contains("interface ZonedDateTime {"), "{slice}");
2559
2560 let full = builtin_docs("Temporal").expect("Temporal");
2561 assert!(
2562 slice.len() * 4 < full.len(),
2563 "slice {} vs full {}",
2564 slice.len(),
2565 full.len()
2566 );
2567 }
2568
2569 #[test]
2570 fn dotted_path_resolves_nested_namespaces_and_values() {
2571 let now = builtin_docs("Temporal.Now").expect("Temporal.Now");
2572 assert!(now.starts_with("namespace Temporal {"));
2573 assert!(now.contains("namespace Now {"));
2574 assert!(now.contains("function instant(): Temporal.Instant;"));
2575
2576 let one = builtin_docs("Temporal.Now.instant").expect("Temporal.Now.instant");
2577 assert!(one.contains("function instant(): Temporal.Instant;"));
2578 assert!(!one.contains("plainDateISO"), "{one}");
2579
2580 let max = builtin_docs("Math.max").expect("Math.max");
2581 assert!(max.contains("function max("), "{max}");
2582 assert!(!max.contains("function min("), "{max}");
2583
2584 let parse = builtin_docs("JSON.parse").expect("JSON.parse");
2585 assert!(parse.contains("function parse("), "{parse}");
2586 assert!(!parse.contains("function stringify("), "{parse}");
2587 }
2588
2589 #[test]
2590 fn dotted_path_resolves_members_of_a_type_head() {
2591 let is_array = builtin_docs("Array.isArray").expect("Array.isArray");
2594 assert!(
2595 is_array.starts_with("interface ArrayConstructor"),
2596 "{is_array}"
2597 );
2598 assert!(is_array.contains("isArray"));
2599
2600 let repeat = builtin_docs("String.repeat").expect("String.repeat");
2601 assert!(repeat.starts_with("interface String"), "{repeat}");
2602 assert!(
2603 repeat.contains("repeat(count: number): string;"),
2604 "{repeat}"
2605 );
2606
2607 let from = builtin_docs("Temporal.Instant.from").expect("Temporal.Instant.from");
2610 assert!(from.starts_with("namespace Temporal {"), "{from}");
2611 assert!(from.contains("interface InstantConstructor {"), "{from}");
2612 assert!(!from.contains("namespace Instant"), "{from}");
2613 }
2614
2615 #[test]
2616 fn every_member_a_miss_names_is_itself_resolvable() {
2617 let b = builtins();
2621 for name in b.types.iter().chain(b.namespaces.iter()) {
2622 let BuiltinLookup::UnknownMember { members, .. } =
2623 builtin_lookup(&format!("{name}.__definitely_not_a_member"))
2624 else {
2625 continue;
2626 };
2627 for member in members {
2628 let path = format!("{name}.{member}");
2629 assert!(
2630 matches!(builtin_lookup(&path), BuiltinLookup::Found(_)),
2631 "`{path}` is offered as a member of `{name}` but does not resolve"
2632 );
2633 }
2634 }
2635 }
2636
2637 #[test]
2638 fn unknown_member_lists_the_members_that_exist() {
2639 let BuiltinLookup::UnknownMember {
2640 path,
2641 member,
2642 members,
2643 } = builtin_lookup("Temporal.Foo")
2644 else {
2645 panic!("expected an unknown-member miss");
2646 };
2647 assert_eq!(path, "Temporal");
2648 assert_eq!(member, "Foo");
2649 assert!(members.contains(&"Instant".to_string()));
2650 assert!(members.contains(&"Now".to_string()));
2651 assert!(
2653 !members.contains(&"InstantConstructor".to_string()),
2654 "{members:?}"
2655 );
2656
2657 let BuiltinLookup::UnknownMember { path, members, .. } = builtin_lookup("Math.max.foo")
2660 else {
2661 panic!("expected an unknown-member miss");
2662 };
2663 assert_eq!(path, "Math.max");
2664 assert!(members.is_empty());
2665 }
2666
2667 #[test]
2668 fn a_bad_member_is_reported_against_its_full_path() {
2669 for (query, expected_path) in [
2670 ("Temporal.Instant.nope", "Temporal.Instant"),
2671 ("Temporal.Now.nope", "Temporal.Now"),
2672 ("Array.nope", "Array"),
2673 ("Temporal.Instant.from.x", "Temporal.Instant.from"),
2674 ("Array.isArray.foo", "Array.isArray"),
2677 ] {
2678 let BuiltinLookup::UnknownMember { path, member, .. } = builtin_lookup(query) else {
2679 panic!("expected an unknown-member miss for {query}");
2680 };
2681 assert_eq!(path, expected_path, "for {query}");
2682 assert!(!member.is_empty(), "for {query}");
2683 }
2684 }
2685
2686 #[test]
2687 fn unknown_head_is_not_an_unknown_member() {
2688 assert_eq!(builtin_lookup("Bogus.Thing"), BuiltinLookup::Unknown);
2689 assert_eq!(builtin_lookup("Temporal#Instant"), BuiltinLookup::Unknown);
2691 assert_eq!(builtin_lookup(""), BuiltinLookup::Unknown);
2692 }
2693
2694 #[test]
2695 fn plain_names_are_unchanged_and_segments_are_case_insensitive() {
2696 let full = builtin_docs("Temporal").expect("Temporal");
2698 assert!(full.starts_with("namespace Temporal {"));
2699 for member in ["Instant", "ZonedDateTime", "PlainDate", "Duration"] {
2700 assert!(
2701 full.contains(&format!("interface {member} {{")),
2702 "missing {member}"
2703 );
2704 }
2705 assert_eq!(
2706 builtin_docs("temporal.instant"),
2707 builtin_docs("Temporal.Instant")
2708 );
2709 assert_eq!(builtin_docs("Temporal."), builtin_docs("Temporal"));
2711 }
2712
2713 #[test]
2714 fn resolve_prefers_a_module_then_falls_back_to_builtins() {
2715 assert!(matches!(resolve("submilli:http"), Resolution::Module(_)));
2716 assert!(matches!(resolve("Temporal"), Resolution::Builtin { .. }));
2717 assert!(matches!(
2718 resolve("Temporal.Instant"),
2719 Resolution::Builtin { .. }
2720 ));
2721 assert!(matches!(
2722 resolve("Temporal.Foo"),
2723 Resolution::UnknownMember { .. }
2724 ));
2725 assert!(matches!(resolve("nope"), Resolution::Unknown));
2726 assert!(matches!(resolve("submilli:security"), Resolution::Unknown));
2727 }
2728
2729 #[test]
2730 fn suggest_reaches_past_bare_edit_distance() {
2731 let extra = vec!["@mcp/linear".to_string()];
2732 assert_eq!(suggest("http", &extra).as_deref(), Some("submilli:http"));
2735 assert_eq!(suggest("htp", &extra).as_deref(), Some("submilli:http"));
2736 assert_eq!(suggest("linear", &extra).as_deref(), Some("@mcp/linear"));
2737 assert_eq!(suggest("Temporel", &extra).as_deref(), Some("Temporal"));
2739 assert_eq!(
2740 suggest("Temporel.Instant", &extra).as_deref(),
2741 Some("Temporal")
2742 );
2743 assert_eq!(suggest("Aray", &extra).as_deref(), Some("Array"));
2744 assert_eq!(suggest("zzzzzzzzz", &extra), None);
2745 assert_ne!(
2748 suggest(INTERNAL_MODULE, &[]).as_deref(),
2749 Some(INTERNAL_MODULE)
2750 );
2751 }
2752
2753 #[test]
2754 fn a_deliberately_omitted_global_points_at_its_replacement() {
2755 assert_eq!(suggest("Date", &[]).as_deref(), Some("Temporal"));
2758 assert_eq!(suggest("date", &[]).as_deref(), Some("Temporal"));
2759 }
2760
2761 #[test]
2762 fn suggest_declines_degenerate_and_self_queries() {
2763 for q in ["", ".", "a", "xy"] {
2766 assert_eq!(suggest(q, &[]), None, "for {q:?}");
2767 }
2768 assert_eq!(suggest(".Temporel", &[]).as_deref(), Some("Temporal"));
2770 let declared = vec!["@acme/mypkg".to_string()];
2772 assert_eq!(suggest("@acme/mypkg", &declared), None);
2773 }
2774
2775 #[test]
2776 fn catalog_is_summary_only_and_bounded() {
2777 let stdlib = catalog(Vec::new());
2778 assert_eq!(stdlib.remaining, 0);
2779 assert!(stdlib.entries.iter().all(|e| e.source == SOURCE_STDLIB));
2780 assert!(stdlib.entries.iter().any(|e| e.name == "submilli:http"));
2781 assert!(stdlib.entries.iter().all(|e| !e.description.is_empty()));
2782 assert!(!stdlib.entries.iter().any(|e| e.name == INTERNAL_MODULE));
2783
2784 let extra: Vec<CatalogEntry> = (0..CATALOG_LIMIT)
2785 .map(|i| CatalogEntry {
2786 name: format!("@acme/p{i}"),
2787 source: "registry".to_string(),
2788 description: "x".to_string(),
2789 })
2790 .collect();
2791 let big = catalog(extra);
2792 assert_eq!(big.entries.len(), CATALOG_LIMIT);
2793 assert_eq!(big.remaining, stdlib.entries.len());
2794 }
2795
2796 #[test]
2797 fn miss_messages_name_the_repair() {
2798 let msg = unknown_member_message("Temporal", "Foo", &["Instant".into(), "Now".into()]);
2799 assert!(msg.contains("Temporal"), "{msg}");
2800 assert!(msg.contains("Instant, Now"), "{msg}");
2801 let leaf = unknown_member_message("Math.max", "foo", &[]);
2802 assert!(leaf.contains("no members"), "{leaf}");
2803 assert!(builtin_no_import_note("Temporal").contains("import"));
2805 }
2806
2807 fn every_package() -> Stdlib {
2810 crate::stdlib::OptionalPackage::ALL
2811 .iter()
2812 .fold(Stdlib::core(), |set, package| set.with(*package))
2813 }
2814
2815 #[test]
2816 fn stdlib_capability_tags_parse_cleanly() {
2817 for module in user_modules(every_package()) {
2818 for symbol in module.values.values() {
2819 let Some(doc) = value_doc(&symbol.kind) else {
2820 continue;
2821 };
2822 for cap in &doc.capabilities {
2823 assert!(
2824 cap.diagnostics.is_empty(),
2825 "{}.{}: {:?}",
2826 module.package_name,
2827 symbol.name,
2828 cap.diagnostics
2829 );
2830 }
2831 }
2832 }
2833 }
2834
2835 #[test]
2836 fn every_importable_stdlib_symbol_has_docs() {
2837 for module in user_modules(every_package()) {
2838 for symbol in module.values.values() {
2839 assert!(
2840 value_doc(&symbol.kind).is_some(),
2841 "{}.{} is missing docs",
2842 module.package_name,
2843 symbol.name
2844 );
2845 }
2846 for symbol in module.types.values() {
2847 assert!(
2848 type_doc(&symbol.kind).is_some(),
2849 "{}.{} is missing docs",
2850 module.package_name,
2851 symbol.name
2852 );
2853 }
2854 }
2855 }
2856
2857 #[test]
2858 fn builtin_docs_print_a_call_signature_without_a_name() {
2859 for name in ["Number", "Number.@call"] {
2860 let docs = builtin_docs(name).expect("Number built-in");
2861 assert!(docs.contains(" (value: "), "{docs}");
2862 assert!(!docs.contains("@call"), "{docs}");
2863 }
2864 }
2865
2866 #[test]
2867 fn builtin_docs_folds_constructor_into_array() {
2868 let docs = builtin_docs("Array").expect("Array built-in");
2869 assert!(docs.contains("interface Array<"));
2870 assert!(docs.contains("interface ArrayConstructor"));
2871 assert!(docs.contains("const Array: ArrayConstructor;"));
2872 }
2873
2874 #[test]
2875 fn builtin_docs_renders_temporal_namespace() {
2876 let docs = builtin_docs("Temporal").expect("Temporal built-in");
2877 assert!(docs.starts_with("namespace Temporal {"));
2878 assert!(docs.contains("interface Instant"));
2879 assert!(docs.contains("namespace Now {"));
2880 assert!(docs.contains(" interface Instant"));
2882 assert_eq!(docs.matches("const Instant").count(), 1, "{docs}");
2885 }
2886
2887 #[test]
2888 fn builtin_docs_handles_json_intrinsic() {
2889 let docs = builtin_docs("JSON").expect("JSON built-in");
2890 assert!(docs.contains(
2891 "function stringify<T>(value: T, replacer?: null, space?: number | string | null): string | undefined;"
2892 ));
2893 assert!(docs.contains("function parse(text: string): unknown;"));
2894 }
2895
2896 #[test]
2897 fn builtin_docs_is_case_insensitive() {
2898 assert_eq!(builtin_docs("array"), builtin_docs("Array"));
2900 assert_eq!(builtin_docs("temporal"), builtin_docs("Temporal"));
2901 assert_eq!(builtin_docs("json"), builtin_docs("JSON"));
2902 assert_eq!(builtin_docs("ARRAY"), builtin_docs("Array"));
2904 }
2905
2906 #[test]
2907 fn builtin_docs_unknown_is_none() {
2908 assert!(builtin_docs("Promise").is_none());
2909 assert!(builtin_docs("Temporal#Instant").is_none());
2910 }
2911
2912 #[test]
2913 fn packages_d_ts_imports_the_types_a_package_borrows() {
2914 let mut package = PackageDeclaration::with_package("@acme/files");
2915 let download = crate::stdlib::http::package_declaration()
2916 .values
2917 .get("download")
2918 .expect("submilli:http declares download")
2919 .clone();
2920 package.values.insert("fetch".to_string(), download);
2921
2922 let docs = render_packages_d_ts(&[&package], &[]);
2923
2924 assert!(docs.starts_with("declare module \"@acme/files\" {\n"));
2925 assert!(docs.contains(" import type { DownloadResult } from \"submilli:http\";\n"));
2926 assert!(docs.contains(" import type { DownloadOptions } from \"submilli:http\";\n"));
2927 assert!(!docs.contains("import type { Response }"));
2928 }
2929
2930 #[test]
2931 fn a_borrowed_type_is_imported_from_its_own_module_when_others_share_its_name() {
2932 let mut leaf = PackageDeclaration::with_package("@acme/leaf");
2935 leaf.types
2936 .insert("Page".to_string(), alias_symbol("@acme/leaf", "Page"));
2937 let mut other = PackageDeclaration::with_package("@acme/other");
2938 other
2939 .types
2940 .insert("Page".to_string(), alias_symbol("@acme/other", "Page"));
2941 let mut mid = PackageDeclaration::with_package("@acme/mid");
2942 let page = reference(
2943 "@acme/leaf",
2944 crate::mangle::package_symbol("@acme/leaf", "Page"),
2945 "Page",
2946 );
2947 mid.values
2948 .insert("page".to_string(), const_symbol("@acme/mid", "page", page));
2949
2950 let docs = render_packages_d_ts(&[&leaf, &other, &mid], &[]);
2951
2952 assert!(docs.contains(" import type { Page } from \"@acme/leaf\";\n"));
2953 assert!(!docs.contains("from \"submilli:session\""));
2954 assert!(!docs.contains("from \"@acme/other\""));
2955 }
2956
2957 fn alias_symbol(package: &str, name: &str) -> crate::TypeSymbol {
2958 crate::TypeSymbol {
2959 name: name.to_string(),
2960 mangled_name: crate::mangle::package_symbol(package, name),
2961 declaration_span: crate::Span::at(crate::FileId(0)),
2962 kind: crate::TypeKind::Alias {
2963 generics: Vec::new(),
2964 ty: Type::Number,
2965 doc: None,
2966 },
2967 }
2968 }
2969
2970 fn const_symbol(package: &str, name: &str, ty: Type) -> ValueSymbol {
2971 ValueSymbol {
2972 name: name.to_string(),
2973 mangled_name: crate::mangle::package_symbol(package, name),
2974 declaration_span: crate::Span::at(crate::FileId(0)),
2975 kind: ValueKind::Const { ty, doc: None },
2976 }
2977 }
2978
2979 fn reference(package: &str, mangled: crate::MangledName, local_name: &str) -> Type {
2980 Type::alias_ref(
2981 crate::types::Package(package.to_string()),
2982 local_name,
2983 mangled,
2984 Vec::new(),
2985 )
2986 }
2987
2988 #[test]
2989 fn an_aliased_type_is_imported_under_the_name_the_text_uses() {
2990 let mut leaf = PackageDeclaration::with_package("@acme/leaf");
2991 leaf.types
2992 .insert("Page".to_string(), alias_symbol("@acme/leaf", "Page"));
2993 let mut mid = PackageDeclaration::with_package("@acme/mid");
2994 let page = reference(
2995 "@acme/leaf",
2996 crate::mangle::package_symbol("@acme/leaf", "Page"),
2997 "LeafPage",
2998 );
2999 mid.values
3000 .insert("page".to_string(), const_symbol("@acme/mid", "page", page));
3001
3002 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3003
3004 assert!(docs.contains(" import type { Page as LeafPage } from \"@acme/leaf\";\n"));
3005 assert!(!docs.contains("declare module \"@acme/leaf\""));
3006 }
3007
3008 #[test]
3009 fn a_type_reexported_from_an_internal_module_is_imported_from_the_package() {
3010 let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3011 leaf.types
3012 .insert("Status".to_string(), alias_symbol("@acme/leaf", "Status"));
3013 let mut mid = PackageDeclaration::with_package("@acme/mid");
3014 let status = reference(
3015 "@acme/leaf",
3016 crate::mangle::package_module_symbol("@acme/leaf", "model", "Status"),
3017 "Status",
3018 );
3019 mid.values.insert(
3020 "status".to_string(),
3021 const_symbol("@acme/mid", "status", status),
3022 );
3023
3024 let docs = render_packages_d_ts(&[&leaf, &mid], &[]);
3025
3026 assert!(docs.contains(" import type { Status } from \"@acme/leaf\";\n"));
3027 }
3028
3029 #[test]
3030 fn only_the_rendered_declarations_bring_imports() {
3031 let mut mid = PackageDeclaration::with_package("@acme/mid");
3032 let response = reference(
3033 "submilli:http",
3034 crate::mangle::package_symbol("submilli:http", "Response"),
3035 "Response",
3036 );
3037 mid.runtime_globals.insert(
3038 crate::mangle::package_symbol("@acme/mid", "hidden"),
3039 response.clone(),
3040 );
3041
3042 let docs = render_packages_d_ts(&[&mid], &[]);
3043
3044 assert!(!docs.contains("import type"), "{docs}");
3045 }
3046
3047 #[test]
3048 fn a_value_named_like_a_borrowed_interface_leaves_its_import() {
3049 let download = reference(
3052 "submilli:http",
3053 crate::mangle::package_symbol("submilli:http", "DownloadResult"),
3054 "DownloadResult",
3055 );
3056 let mut mid = PackageDeclaration::with_package("@acme/mid");
3057 mid.values.insert(
3058 "DownloadResult".to_string(),
3059 const_symbol("@acme/mid", "DownloadResult", download),
3060 );
3061
3062 let docs = render_packages_d_ts(&[&mid], &[]);
3063
3064 assert!(
3065 docs.contains(" import type { DownloadResult } from \"submilli:http\";\n"),
3066 "{docs}"
3067 );
3068 }
3069
3070 #[test]
3071 fn type_arguments_bring_imports_and_alias_bodies_do_not() {
3072 let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3073 leaf.types
3074 .insert("Page".to_string(), alias_symbol("@acme/leaf", "Page"));
3075 leaf.types
3076 .insert("Item".to_string(), alias_symbol("@acme/leaf", "Item"));
3077 let mut mid = PackageDeclaration::with_package("@acme/mid");
3078 let item = reference(
3079 "@acme/leaf",
3080 crate::mangle::package_symbol("@acme/leaf", "Item"),
3081 "Item",
3082 );
3083 let page_of_items = Type::alias_ref(
3084 crate::types::Package("@acme/leaf".to_string()),
3085 "Page",
3086 crate::mangle::package_symbol("@acme/leaf", "Page"),
3087 vec![Type::Array(Box::new(item))],
3088 );
3089 mid.values.insert(
3090 "page".to_string(),
3091 const_symbol("@acme/mid", "page", page_of_items),
3092 );
3093 let response = reference(
3096 "submilli:http",
3097 crate::mangle::package_symbol("submilli:http", "Response"),
3098 "Response",
3099 );
3100 let wrapped = Type::Alias {
3101 mangled: crate::mangle::package_symbol("@acme/leaf", "Item"),
3102 package: crate::types::Package("@acme/leaf".to_string()),
3103 name: "Item".to_string(),
3104 args: Vec::new(),
3105 ty: Box::new(response),
3106 };
3107 mid.values.insert(
3108 "item".to_string(),
3109 const_symbol("@acme/mid", "item", wrapped),
3110 );
3111
3112 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3113
3114 assert!(docs.contains(" import type { Page } from \"@acme/leaf\";\n"));
3115 assert!(docs.contains(" import type { Item } from \"@acme/leaf\";\n"));
3116 assert!(!docs.contains("Response"), "{docs}");
3117 }
3118
3119 fn class_symbol(
3120 package: &str,
3121 name: &str,
3122 extends: Option<crate::ClassExtends>,
3123 ) -> crate::TypeSymbol {
3124 crate::TypeSymbol {
3125 name: name.to_string(),
3126 mangled_name: crate::mangle::package_symbol(package, name),
3127 declaration_span: crate::Span::at(crate::FileId(0)),
3128 kind: TypeKind::Class {
3129 generics: Vec::new(),
3130 fields: BTreeMap::new(),
3131 narrowing_checks: BTreeMap::new(),
3132 methods: BTreeMap::new(),
3133 method_visibility: BTreeMap::new(),
3134 accessors: Vec::new(),
3135 constructor: Vec::new(),
3136 constructor_visibility: crate::Visibility::Public,
3137 statics: BTreeMap::new(),
3138 static_visibility: BTreeMap::new(),
3139 static_fields: BTreeMap::new(),
3140 extends,
3141 implements: Vec::new(),
3142 doc: None,
3143 },
3144 }
3145 }
3146
3147 #[test]
3148 fn a_class_renders_and_imports_its_parent() {
3149 let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3150 leaf.types
3151 .insert("Base".to_string(), class_symbol("@acme/leaf", "Base", None));
3152 leaf.types
3153 .insert("Item".to_string(), alias_symbol("@acme/leaf", "Item"));
3154 let item = reference(
3155 "@acme/leaf",
3156 crate::mangle::package_symbol("@acme/leaf", "Item"),
3157 "Item",
3158 );
3159 let extends = crate::ClassExtends {
3160 parent: crate::mangle::package_symbol("@acme/leaf", "Base"),
3161 args: vec![item],
3162 };
3163 let mut sub = class_symbol("@acme/mid", "Sub", Some(extends));
3164 if let TypeKind::Class { fields, .. } = &mut sub.kind {
3165 let response = reference(
3167 "submilli:http",
3168 crate::mangle::package_symbol("submilli:http", "Response"),
3169 "Response",
3170 );
3171 fields.insert(
3172 "hidden".to_string(),
3173 crate::FieldSig {
3174 ty: response,
3175 visibility: crate::Visibility::Private,
3176 readonly: false,
3177 optional: false,
3178 doc: None,
3179 },
3180 );
3181 }
3182 let mut mid = PackageDeclaration::with_package("@acme/mid");
3183 mid.types.insert("Sub".to_string(), sub);
3184
3185 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3186
3187 assert!(
3188 docs.contains(" import type { Base } from \"@acme/leaf\";\n"),
3189 "{docs}"
3190 );
3191 assert!(
3192 docs.contains(" import type { Item } from \"@acme/leaf\";\n"),
3193 "{docs}"
3194 );
3195 assert!(
3196 docs.contains(" export class Sub extends Base<Item> {"),
3197 "{docs}"
3198 );
3199 assert!(!docs.contains("Response"), "{docs}");
3200 }
3201
3202 fn subclass_of(package: &str, name: &str, parent: crate::MangledName) -> crate::TypeSymbol {
3203 class_symbol(package, name, Some(crate::ClassExtends::plain(parent)))
3204 }
3205
3206 fn two_bases() -> (PackageDeclaration, PackageDeclaration) {
3208 let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3209 leaf.types
3210 .insert("Base".to_string(), class_symbol("@acme/leaf", "Base", None));
3211 let mut other = PackageDeclaration::with_package("@acme/other");
3212 other.types.insert(
3213 "Base".to_string(),
3214 class_symbol("@acme/other", "Base", None),
3215 );
3216 (leaf, other)
3217 }
3218
3219 #[test]
3220 fn a_parent_named_like_a_local_value_is_imported_under_another_name() {
3221 let (leaf, _) = two_bases();
3222 let mut package = PackageDeclaration::with_package("@acme/shadowed");
3223 package.values.insert(
3224 "Base".to_string(),
3225 const_symbol("@acme/shadowed", "Base", Type::Number),
3226 );
3227 let base = crate::mangle::package_symbol("@acme/leaf", "Base");
3228 package.types.insert(
3229 "Sub".to_string(),
3230 subclass_of("@acme/shadowed", "Sub", base),
3231 );
3232
3233 let docs = render_packages_d_ts(&[&package], &[&leaf]);
3234
3235 assert!(
3236 docs.contains(" import type { Base as Base1 } from \"@acme/leaf\";\n"),
3237 "{docs}"
3238 );
3239 assert!(
3240 docs.contains(" export class Sub extends Base1 {"),
3241 "{docs}"
3242 );
3243 }
3244
3245 #[test]
3246 fn parents_that_share_a_public_name_get_different_local_names() {
3247 let (leaf, other) = two_bases();
3248 let mut package = PackageDeclaration::with_package("@acme/both");
3249 package.types.insert(
3250 "A".to_string(),
3251 subclass_of(
3252 "@acme/both",
3253 "A",
3254 crate::mangle::package_symbol("@acme/leaf", "Base"),
3255 ),
3256 );
3257 package.types.insert(
3258 "B".to_string(),
3259 subclass_of(
3260 "@acme/both",
3261 "B",
3262 crate::mangle::package_symbol("@acme/other", "Base"),
3263 ),
3264 );
3265
3266 let docs = render_packages_d_ts(&[&package], &[&leaf, &other]);
3267
3268 assert!(
3269 docs.contains(" import type { Base } from \"@acme/leaf\";\n"),
3270 "{docs}"
3271 );
3272 assert!(
3273 docs.contains(" import type { Base as Base1 } from \"@acme/other\";\n"),
3274 "{docs}"
3275 );
3276 assert!(docs.contains(" export class A extends Base {"), "{docs}");
3277 assert!(docs.contains(" export class B extends Base1 {"), "{docs}");
3278 }
3279
3280 #[test]
3281 fn a_parent_already_imported_under_an_alias_keeps_it() {
3282 let (leaf, _) = two_bases();
3283 let base = crate::mangle::package_symbol("@acme/leaf", "Base");
3284 let mut package = PackageDeclaration::with_package("@acme/aliased");
3285 package.values.insert(
3286 "base".to_string(),
3287 const_symbol(
3288 "@acme/aliased",
3289 "base",
3290 reference("@acme/leaf", base.clone(), "LB"),
3291 ),
3292 );
3293 package
3294 .types
3295 .insert("Sub".to_string(), subclass_of("@acme/aliased", "Sub", base));
3296
3297 let docs = render_packages_d_ts(&[&package], &[&leaf]);
3298
3299 assert!(
3300 docs.contains(" import type { Base as LB } from \"@acme/leaf\";\n"),
3301 "{docs}"
3302 );
3303 assert!(docs.contains(" export class Sub extends LB {"), "{docs}");
3304 assert!(!docs.contains("import type { Base }"), "{docs}");
3305 }
3306
3307 #[test]
3308 fn a_class_extending_a_builtin_error_renders_its_parent_as_the_global() {
3309 let error = builtin_package_declaration()
3310 .types
3311 .get("Error")
3312 .expect("the prelude declares Error")
3313 .mangled_name
3314 .clone();
3315 let mut package = PackageDeclaration::with_package("@acme/errors");
3316 package.types.insert(
3317 "BillingError".to_string(),
3318 subclass_of("@acme/errors", "BillingError", error),
3319 );
3320
3321 let docs = render_packages_d_ts(&[&package], &[]);
3322
3323 assert!(
3324 docs.contains(" export class BillingError extends Error {"),
3325 "{docs}"
3326 );
3327 assert!(!docs.contains("import type"), "{docs}");
3328 }
3329
3330 #[test]
3331 fn a_package_value_named_like_the_global_parent_leaves_the_class_without_it() {
3332 let error = builtin_package_declaration()
3333 .types
3334 .get("Error")
3335 .expect("the prelude declares Error")
3336 .mangled_name
3337 .clone();
3338 let mut package = PackageDeclaration::with_package("@acme/errors");
3339 package.values.insert(
3340 "Error".to_string(),
3341 const_symbol("@acme/errors", "Error", Type::Number),
3342 );
3343 package.types.insert(
3344 "BillingError".to_string(),
3345 subclass_of("@acme/errors", "BillingError", error),
3346 );
3347
3348 let docs = render_packages_d_ts(&[&package], &[]);
3349
3350 assert!(docs.contains(" export class BillingError {"), "{docs}");
3351 }
3352
3353 #[test]
3354 fn builtin_errors_extend_error_in_the_editor_declarations() {
3355 let lib = render_lib_submilli_d_ts();
3356 for class in [
3357 "QuotaExceededError",
3358 "RangeError",
3359 "TypeError",
3360 "SyntaxError",
3361 "URIError",
3362 "ReferenceError",
3363 "PermissionDeniedError",
3364 ] {
3365 assert!(
3366 lib.contains(&format!("declare class {class} extends Error {{")),
3367 "{class}"
3368 );
3369 }
3370 assert!(lib.contains("declare class Error {"));
3371 }
3372
3373 #[test]
3374 fn a_value_named_like_a_borrowed_class_keeps_the_class_out() {
3375 let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3377 leaf.types
3378 .insert("Base".to_string(), class_symbol("@acme/leaf", "Base", None));
3379 let base = Type::class_ref(
3380 crate::types::Package("@acme/leaf".to_string()),
3381 "Base",
3382 crate::mangle::package_symbol("@acme/leaf", "Base"),
3383 Vec::new(),
3384 );
3385 let mut mid = PackageDeclaration::with_package("@acme/mid");
3386 mid.values
3387 .insert("Base".to_string(), const_symbol("@acme/mid", "Base", base));
3388
3389 let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3390
3391 assert!(!docs.contains("import type"), "{docs}");
3392 }
3393
3394 #[test]
3395 fn a_package_type_named_like_a_constructor_is_an_ordinary_type() {
3396 let mut package = PackageDeclaration::with_package("@acme/leaf");
3397 package
3398 .types
3399 .insert("Client".to_string(), alias_symbol("@acme/leaf", "Client"));
3400 package.types.insert(
3401 "ClientConstructor".to_string(),
3402 alias_symbol("@acme/leaf", "ClientConstructor"),
3403 );
3404
3405 let docs = render_packages_d_ts(&[&package], &[]);
3406
3407 assert!(docs.contains(" export type Client = number;"), "{docs}");
3408 assert!(
3409 docs.contains(" export type ClientConstructor = number;"),
3410 "{docs}"
3411 );
3412 assert!(!docs.contains("const Client"), "{docs}");
3413 }
3414
3415 #[test]
3416 fn stdlib_d_ts_declares_each_user_module() {
3417 let docs = every_package().render_stdlib_d_ts();
3418 for module in user_modules(every_package()) {
3419 assert!(
3420 docs.contains(&format!("declare module \"{}\" {{", module.package_name)),
3421 "missing declare module for {}",
3422 module.package_name
3423 );
3424 }
3425 assert!(docs.contains("declare module \"submilli:security\" {\n"));
3426 assert!(docs.contains(" export function check<T>("));
3427 assert!(docs.contains("declare module \"submilli:uuid\" {\n"));
3428 assert!(docs.contains(" export function v4("));
3429 assert!(docs.contains(" function delete_("));
3430 assert!(docs.contains(" export { delete_ as delete };"));
3431 assert!(!docs.contains("export function delete("));
3432 }
3433
3434 #[test]
3435 fn lib_submilli_d_ts_renders_ambient_builtins() {
3436 let docs = render_lib_submilli_d_ts();
3437 assert!(docs.contains("interface Array<"));
3438 assert!(docs.contains("interface ArrayConstructor"));
3439 assert!(docs.contains("declare const Array: ArrayConstructor;"));
3440 assert_eq!(
3441 docs.matches("declare const Array: ArrayConstructor;")
3442 .count(),
3443 1
3444 );
3445 assert!(docs.contains("declare const console: Console;"));
3446 assert!(docs.contains("declare namespace Math {"));
3447 assert!(docs.contains("declare namespace Temporal {"));
3448 assert!(docs.contains("declare namespace JSON {"));
3449 assert!(docs.contains(
3450 "function stringify<T>(value: T, replacer?: null, space?: number | string | null): string | undefined;"
3451 ));
3452 assert!(docs.contains("interface Function {}"));
3453 assert!(docs.contains("interface IArguments {}"));
3454 assert!(docs.contains("declare class Error {"));
3455 assert!(docs.contains("static isError(value?: unknown): value is Error;"));
3456 assert!(!docs.contains("ErrorConstructor"));
3457 assert!(!docs.contains("declare module"));
3458 assert!(!docs.contains("function string_concat"));
3459 }
3460
3461 #[test]
3462 fn d_ts_renderer_uses_typescript_call_and_predicate_signatures() {
3463 let docs = render_lib_submilli_d_ts();
3464 assert!(docs.contains("(value: string | bigint | undefined): number;"));
3465 assert!(docs.contains("isArray<T>(value: T): value is T & unknown[];"));
3466 assert!(!docs.contains("@call"));
3467 }
3468
3469 fn class_with_constructor(
3470 package: &str,
3471 name: &str,
3472 constructor: Vec<Param>,
3473 constructor_visibility: crate::Visibility,
3474 extends: Option<ClassExtends>,
3475 ) -> TypeSymbol {
3476 TypeSymbol {
3477 name: name.to_string(),
3478 mangled_name: crate::mangle::package_symbol(package, name),
3479 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3480 kind: TypeKind::Class {
3481 generics: Vec::new(),
3482 fields: BTreeMap::new(),
3483 narrowing_checks: BTreeMap::new(),
3484 methods: BTreeMap::new(),
3485 method_visibility: BTreeMap::new(),
3486 accessors: Vec::new(),
3487 constructor,
3488 constructor_visibility,
3489 statics: BTreeMap::new(),
3490 static_visibility: BTreeMap::new(),
3491 static_fields: BTreeMap::new(),
3492 extends,
3493 implements: Vec::new(),
3494 doc: None,
3495 },
3496 }
3497 }
3498
3499 #[test]
3500 fn a_private_constructor_stays_out_of_the_docs_and_is_protected_in_d_ts() {
3501 let package = "@acme/shapes";
3502 let mut defs = PackageDeclaration::with_package(package);
3503 let point = class_with_constructor(
3504 package,
3505 "Point",
3506 vec![Param::new("secret", Type::String)],
3507 crate::Visibility::Private,
3508 None,
3509 );
3510 let labeled = class_with_constructor(
3511 package,
3512 "Labeled",
3513 vec![Param::new("secret", Type::String)],
3514 crate::Visibility::Private,
3515 Some(ClassExtends {
3516 parent: point.mangled_name.clone(),
3517 args: Vec::new(),
3518 }),
3519 );
3520 defs.types.insert("Point".to_string(), point);
3521 defs.types.insert("Labeled".to_string(), labeled);
3522
3523 let docs = render_declarations(&defs);
3524 assert!(docs.contains("class Point"), "{docs}");
3525 assert!(!docs.contains("constructor"), "{docs}");
3526
3527 let d_ts = render_packages_d_ts(&[&defs], &[]);
3530 assert!(d_ts.contains("class Labeled extends Point"), "{d_ts}");
3531 assert_eq!(
3532 d_ts.matches("protected constructor();").count(),
3533 2,
3534 "{d_ts}"
3535 );
3536 assert!(!d_ts.contains("secret"), "{d_ts}");
3537 }
3538
3539 #[test]
3540 fn constructor_visibility_round_trips_and_a_public_one_is_not_written() {
3541 let package = "@acme/shapes";
3542 for visibility in [crate::Visibility::Private, crate::Visibility::Public] {
3543 let symbol = class_with_constructor(package, "Point", Vec::new(), visibility, None);
3544 let json = serde_json::to_string(&symbol.kind).unwrap();
3545 assert_eq!(
3546 json.contains("constructor_visibility"),
3547 !visibility.is_public()
3548 );
3549 let TypeKind::Class {
3550 constructor_visibility,
3551 ..
3552 } = serde_json::from_str::<TypeKind>(&json).unwrap()
3553 else {
3554 panic!("not a class: {json}");
3555 };
3556 assert_eq!(constructor_visibility, visibility);
3557 }
3558 }
3559
3560 #[test]
3561 fn class_declarations_omit_private_members() {
3562 use crate::{FieldSig, MethodSig, Visibility};
3563
3564 let field = |ty, visibility| FieldSig {
3565 ty,
3566 visibility,
3567 readonly: false,
3568 optional: false,
3569 doc: None,
3570 };
3571 let method = |ret| MethodSig {
3572 optional: false,
3573 generics: Vec::new(),
3574 params: Vec::new(),
3575 ret,
3576 predicate: None,
3577 doc: None,
3578 };
3579
3580 let mut fields = BTreeMap::new();
3581 fields.insert("name".to_string(), field(Type::String, Visibility::Public));
3582 fields.insert(
3583 "sound".to_string(),
3584 field(Type::String, Visibility::Private),
3585 );
3586 let mut methods = BTreeMap::new();
3587 methods.insert("speak".to_string(), method(Type::String));
3588 methods.insert("secret".to_string(), method(Type::Number));
3589 let mut method_visibility = BTreeMap::new();
3590 method_visibility.insert("speak".to_string(), Visibility::Public);
3591 method_visibility.insert("secret".to_string(), Visibility::Private);
3592
3593 let mut defs = PackageDeclaration::with_package("@acme/zoo");
3594 defs.types.insert(
3595 "Animal".to_string(),
3596 TypeSymbol {
3597 name: "Animal".to_string(),
3598 mangled_name: crate::mangle::package_symbol("@acme/zoo", "Animal"),
3599 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3600 kind: TypeKind::Class {
3601 generics: Vec::new(),
3602 fields,
3603 narrowing_checks: BTreeMap::new(),
3604 methods,
3605 method_visibility,
3606 accessors: Vec::new(),
3607 constructor: vec![Param::new("name", Type::String)],
3608 constructor_visibility: crate::Visibility::Public,
3609 statics: BTreeMap::new(),
3610 static_visibility: BTreeMap::new(),
3611 static_fields: BTreeMap::new(),
3612 extends: None,
3613 implements: Vec::new(),
3614 doc: None,
3615 },
3616 },
3617 );
3618
3619 let rendered = render_declarations(&defs);
3620 assert!(rendered.contains("class Animal {"), "{rendered}");
3622 assert!(rendered.contains("name: string;"), "{rendered}");
3623 assert!(rendered.contains("speak(): string;"), "{rendered}");
3624 assert!(
3625 rendered.contains("constructor(name: string);"),
3626 "{rendered}"
3627 );
3628 assert!(!rendered.contains("private "), "{rendered}");
3630 assert!(!rendered.contains("sound"), "{rendered}");
3631 assert!(!rendered.contains("secret"), "{rendered}");
3632 }
3633
3634 #[test]
3635 fn class_declarations_render_accessors_faithfully() {
3636 use crate::{AccessorSig, FieldSig, Param, Visibility};
3637
3638 let prop = |ty: Type, readonly| FieldSig {
3641 ty,
3642 visibility: Visibility::Public,
3643 readonly,
3644 optional: false,
3645 doc: None,
3646 };
3647 let mut fields = BTreeMap::new();
3648 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 {
3654 name: name.to_string(),
3655 param: Param::new("v", ty),
3656 };
3657 let getter = |name: &str, ty: Type| AccessorSig::Getter {
3658 name: name.to_string(),
3659 ret_ty: ty,
3660 };
3661 let accessors = vec![
3662 getter("area", Type::Number),
3663 getter("size", Type::Number),
3664 setter("size", Type::Number),
3665 getter("label", Type::String),
3666 setter("label", Type::Number),
3667 setter("secret", Type::Number),
3668 ];
3669
3670 let mut defs = PackageDeclaration::with_package("@acme/geo");
3671 defs.types.insert(
3672 "Shape".to_string(),
3673 TypeSymbol {
3674 name: "Shape".to_string(),
3675 mangled_name: crate::mangle::package_symbol("@acme/geo", "Shape"),
3676 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3677 kind: TypeKind::Class {
3678 generics: Vec::new(),
3679 fields,
3680 narrowing_checks: BTreeMap::new(),
3681 methods: BTreeMap::new(),
3682 method_visibility: BTreeMap::new(),
3683 accessors,
3684 constructor: Vec::new(),
3685 constructor_visibility: crate::Visibility::Public,
3686 statics: BTreeMap::new(),
3687 static_visibility: BTreeMap::new(),
3688 static_fields: BTreeMap::new(),
3689 extends: None,
3690 implements: Vec::new(),
3691 doc: None,
3692 },
3693 },
3694 );
3695
3696 let docs = render_declarations(&defs);
3697 let shape = &defs.types.get("Shape").unwrap().kind;
3698 let mut dts = String::new();
3699 render_ts_type(&mut dts, "Shape", shape, "", "export ", None);
3700 for rendered in [&docs, &dts] {
3701 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}");
3705 assert!(rendered.contains("set secret(v: number);"), "{rendered}"); assert!(!rendered.contains("get area"), "{rendered}");
3708 assert!(!rendered.contains("get size("), "{rendered}");
3709 }
3710 }
3711
3712 #[test]
3713 fn class_declarations_render_public_statics_only() {
3714 use crate::{FieldSig, MethodSig, Param, Visibility};
3715
3716 let method = |ret: Type| MethodSig {
3717 optional: false,
3718 generics: Vec::new(),
3719 params: vec![Param::new("x", Type::Number)],
3720 ret,
3721 predicate: None,
3722 doc: None,
3723 };
3724 let mut statics = BTreeMap::new();
3725 statics.insert("make".to_string(), method(Type::Number));
3726 statics.insert("hidden".to_string(), method(Type::Number));
3727 let mut static_visibility = BTreeMap::new();
3728 static_visibility.insert("make".to_string(), Visibility::Public);
3729 static_visibility.insert("hidden".to_string(), Visibility::Private);
3730 let mut static_fields = BTreeMap::new();
3731 static_fields.insert(
3732 "MAX".to_string(),
3733 FieldSig {
3734 ty: Type::Number,
3735 visibility: Visibility::Public,
3736 readonly: true,
3737 optional: false,
3738 doc: None,
3739 },
3740 );
3741 static_fields.insert(
3742 "KEY".to_string(),
3743 FieldSig {
3744 ty: Type::Number,
3745 visibility: Visibility::Private,
3746 readonly: true,
3747 optional: false,
3748 doc: None,
3749 },
3750 );
3751 static_fields.insert(
3752 "count".to_string(),
3753 FieldSig {
3754 ty: Type::Number,
3755 visibility: Visibility::Public,
3756 readonly: false,
3757 optional: false,
3758 doc: None,
3759 },
3760 );
3761 static_fields.insert(
3762 "seed".to_string(),
3763 FieldSig {
3764 ty: Type::Number,
3765 visibility: Visibility::Private,
3766 readonly: false,
3767 optional: false,
3768 doc: None,
3769 },
3770 );
3771
3772 let mut defs = PackageDeclaration::with_package("@acme/calc");
3773 defs.types.insert(
3774 "Calc".to_string(),
3775 TypeSymbol {
3776 name: "Calc".to_string(),
3777 mangled_name: crate::mangle::package_symbol("@acme/calc", "Calc"),
3778 declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3779 kind: TypeKind::Class {
3780 generics: Vec::new(),
3781 fields: BTreeMap::new(),
3782 narrowing_checks: BTreeMap::new(),
3783 methods: BTreeMap::new(),
3784 method_visibility: BTreeMap::new(),
3785 accessors: Vec::new(),
3786 constructor: Vec::new(),
3787 constructor_visibility: crate::Visibility::Public,
3788 statics,
3789 static_visibility,
3790 static_fields,
3791 extends: None,
3792 implements: Vec::new(),
3793 doc: None,
3794 },
3795 },
3796 );
3797
3798 let docs = render_declarations(&defs);
3799 let calc = &defs.types.get("Calc").unwrap().kind;
3800 let mut dts = String::new();
3801 render_ts_type(&mut dts, "Calc", calc, "", "export ", None);
3802 for rendered in [&docs, &dts] {
3803 assert!(
3804 rendered.contains("static make(x: number): number;"),
3805 "{rendered}"
3806 );
3807 assert!(
3808 rendered.contains("static readonly MAX: number;"),
3809 "{rendered}"
3810 );
3811 assert!(rendered.contains("static count: number;"), "{rendered}");
3812 assert!(!rendered.contains("hidden"), "{rendered}");
3813 assert!(!rendered.contains("KEY"), "{rendered}");
3814 assert!(!rendered.contains("seed"), "{rendered}");
3815 }
3816 }
3817}