1use serde::{Deserialize, Serialize};
2use shape_ast::ast::{
3 DocComment, ExportItem, FunctionDef, InterfaceMember, Item, Program, Span, TraitMember,
4 TypeAnnotation,
5};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9pub enum DocItemKind {
10 Function,
11 Type,
12 Interface,
13 Enum,
14 Trait,
15 Field,
16 Variant,
17 Method,
18 AssociatedType,
19 Constant,
20 Module,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct DocParam {
25 pub name: String,
26 pub type_name: Option<String>,
27 pub description: Option<String>,
28 pub default_value: Option<String>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DocItem {
33 pub kind: DocItemKind,
34 pub name: String,
35 pub doc: String,
36 pub signature: Option<String>,
37 pub type_params: Vec<String>,
38 pub params: Vec<DocParam>,
39 pub return_type: Option<String>,
40 pub children: Vec<DocItem>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, Default)]
44pub struct PackageDocs {
45 pub readme: Option<String>,
46 pub modules: HashMap<String, Vec<DocItem>>,
47}
48
49pub fn extract_docs_from_ast(_source: &str, ast: &Program) -> Vec<DocItem> {
50 let mut docs = Vec::new();
51 collect_items(&ast.items, ast, &[], &mut docs);
52 docs
53}
54
55fn collect_items(
56 items: &[Item],
57 program: &Program,
58 module_path: &[String],
59 docs: &mut Vec<DocItem>,
60) {
61 for item in items {
62 match item {
63 Item::Module(module, span) => {
64 let path = join_path(module_path, &module.name);
65 if let Some(comment) = program.docs.comment_for_span(*span) {
66 docs.push(DocItem {
67 kind: DocItemKind::Module,
68 name: path.clone(),
69 doc: doc_text(comment),
70 signature: None,
71 type_params: Vec::new(),
72 params: Vec::new(),
73 return_type: None,
74 children: Vec::new(),
75 });
76 }
77
78 let mut next_path = module_path.to_vec();
79 next_path.push(module.name.clone());
80 collect_items(&module.items, program, &next_path, docs);
81 }
82 Item::Function(function, span) => {
83 docs.push(extract_function_doc(
84 program,
85 join_path(module_path, &function.name),
86 function,
87 *span,
88 ));
89 }
90 Item::ForeignFunction(function, span) => {
91 docs.push(extract_function_doc(
92 program,
93 join_path(module_path, &function.name),
94 &FunctionDef {
95 name: function.name.clone(),
96 name_span: function.name_span,
97 declaring_module_path: None,
98 doc_comment: function.doc_comment.clone(),
99 type_params: function.type_params.clone(),
100 params: function.params.clone(),
101 return_type: function.return_type.clone(),
102 where_clause: None,
103 body: Vec::new(),
104 annotations: function.annotations.clone(),
105 is_async: function.is_async,
106 is_comptime: false,
107 },
108 *span,
109 ));
110 }
111 Item::StructType(struct_def, span) => {
112 docs.push(extract_struct_doc(
113 program,
114 join_path(module_path, &struct_def.name),
115 struct_def,
116 *span,
117 ));
118 }
119 Item::Enum(enum_def, span) => {
120 docs.push(extract_enum_doc(
121 program,
122 join_path(module_path, &enum_def.name),
123 enum_def,
124 *span,
125 ));
126 }
127 Item::Trait(trait_def, span) => {
128 docs.push(extract_trait_doc(
129 program,
130 join_path(module_path, &trait_def.name),
131 trait_def,
132 *span,
133 ));
134 }
135 Item::Interface(interface_def, span) => {
136 docs.push(extract_interface_doc(
137 program,
138 join_path(module_path, &interface_def.name),
139 interface_def,
140 *span,
141 ));
142 }
143 Item::TypeAlias(alias, span) => {
144 let path = join_path(module_path, &alias.name);
145 docs.push(DocItem {
146 kind: DocItemKind::Type,
147 name: path.clone(),
148 doc: doc_text_from_span(program, *span),
149 signature: Some(format!(
150 "type {} = {}",
151 alias.name,
152 format_type_annotation(&alias.type_annotation)
153 )),
154 type_params: format_type_params(&alias.type_params),
155 params: Vec::new(),
156 return_type: Some(format_type_annotation(&alias.type_annotation)),
157 children: Vec::new(),
158 });
159 }
160 Item::BuiltinFunctionDecl(func, span) => {
161 docs.push(DocItem {
162 kind: DocItemKind::Function,
163 name: join_path(module_path, &func.name),
164 doc: doc_text_from_span(program, *span),
165 signature: Some(format_builtin_signature(func)),
166 type_params: format_type_params(&func.type_params),
167 params: func
168 .params
169 .iter()
170 .map(|param| DocParam {
171 name: param.simple_name().unwrap_or("_").to_string(),
172 type_name: param.type_annotation.as_ref().map(format_type_annotation),
173 description: program
174 .docs
175 .comment_for_span(*span)
176 .and_then(|doc| doc.param_doc(param.simple_name().unwrap_or("_")))
177 .map(str::to_string),
178 default_value: None,
179 })
180 .collect(),
181 return_type: Some(format_type_annotation(&func.return_type)),
182 children: Vec::new(),
183 });
184 }
185 Item::BuiltinTypeDecl(ty, span) => {
186 docs.push(DocItem {
187 kind: DocItemKind::Type,
188 name: join_path(module_path, &ty.name),
189 doc: doc_text_from_span(program, *span),
190 signature: Some(format!("builtin type {}", ty.name)),
191 type_params: format_type_params(&ty.type_params),
192 params: Vec::new(),
193 return_type: None,
194 children: Vec::new(),
195 });
196 }
197 Item::Export(export, span) => match &export.item {
198 ExportItem::Function(function) => {
199 docs.push(extract_function_doc(
200 program,
201 join_path(module_path, &function.name),
202 function,
203 *span,
204 ));
205 }
206 ExportItem::BuiltinFunction(function) => {
207 docs.push(DocItem {
208 kind: DocItemKind::Function,
209 name: join_path(module_path, &function.name),
210 doc: doc_text_from_span(program, *span),
211 signature: Some(format_builtin_signature(function)),
212 type_params: format_type_params(&function.type_params),
213 params: function
214 .params
215 .iter()
216 .map(|param| DocParam {
217 name: param.simple_name().unwrap_or("_").to_string(),
218 type_name: param
219 .type_annotation
220 .as_ref()
221 .map(format_type_annotation),
222 description: program
223 .docs
224 .comment_for_span(*span)
225 .and_then(|doc| {
226 doc.param_doc(param.simple_name().unwrap_or("_"))
227 })
228 .map(str::to_string),
229 default_value: None,
230 })
231 .collect(),
232 return_type: Some(format_type_annotation(&function.return_type)),
233 children: Vec::new(),
234 });
235 }
236 ExportItem::ForeignFunction(function) => {
237 docs.push(DocItem {
238 kind: DocItemKind::Function,
239 name: join_path(module_path, &function.name),
240 doc: doc_text_from_span(program, *span),
241 signature: Some(format_foreign_signature(function)),
242 type_params: format_type_params(&function.type_params),
243 params: function
244 .params
245 .iter()
246 .map(|param| DocParam {
247 name: param.simple_name().unwrap_or("_").to_string(),
248 type_name: param
249 .type_annotation
250 .as_ref()
251 .map(format_type_annotation),
252 description: program
253 .docs
254 .comment_for_span(*span)
255 .and_then(|doc| {
256 doc.param_doc(param.simple_name().unwrap_or("_"))
257 })
258 .map(str::to_string),
259 default_value: None,
260 })
261 .collect(),
262 return_type: function.return_type.as_ref().map(format_type_annotation),
263 children: Vec::new(),
264 });
265 }
266 ExportItem::Struct(struct_def) => {
267 docs.push(extract_struct_doc(
268 program,
269 join_path(module_path, &struct_def.name),
270 struct_def,
271 *span,
272 ));
273 }
274 ExportItem::Enum(enum_def) => {
275 docs.push(extract_enum_doc(
276 program,
277 join_path(module_path, &enum_def.name),
278 enum_def,
279 *span,
280 ));
281 }
282 ExportItem::Trait(trait_def) => {
283 docs.push(extract_trait_doc(
284 program,
285 join_path(module_path, &trait_def.name),
286 trait_def,
287 *span,
288 ));
289 }
290 ExportItem::Interface(interface_def) => {
291 docs.push(extract_interface_doc(
292 program,
293 join_path(module_path, &interface_def.name),
294 interface_def,
295 *span,
296 ));
297 }
298 ExportItem::TypeAlias(alias) => {
299 docs.push(DocItem {
300 kind: DocItemKind::Type,
301 name: join_path(module_path, &alias.name),
302 doc: doc_text_from_span(program, *span),
303 signature: Some(format!(
304 "type {} = {}",
305 alias.name,
306 format_type_annotation(&alias.type_annotation)
307 )),
308 type_params: format_type_params(&alias.type_params),
309 params: Vec::new(),
310 return_type: Some(format_type_annotation(&alias.type_annotation)),
311 children: Vec::new(),
312 });
313 }
314 ExportItem::BuiltinType(ty) => {
315 docs.push(DocItem {
316 kind: DocItemKind::Type,
317 name: join_path(module_path, &ty.name),
318 doc: doc_text_from_span(program, *span),
319 signature: Some(format!("builtin type {}", ty.name)),
320 type_params: format_type_params(&ty.type_params),
321 params: Vec::new(),
322 return_type: None,
323 children: Vec::new(),
324 });
325 }
326 ExportItem::Annotation(_) => {}
327 ExportItem::Named(_) => {}
328 },
329 _ => {}
330 }
331 }
332}
333
334fn extract_function_doc(
335 program: &Program,
336 path: String,
337 func: &FunctionDef,
338 span: Span,
339) -> DocItem {
340 let doc = program.docs.comment_for_span(span);
341 let params = func
342 .params
343 .iter()
344 .map(|param| {
345 let name = param.simple_name().unwrap_or("_").to_string();
346 DocParam {
347 description: doc.and_then(|d| d.param_doc(&name)).map(str::to_string),
348 default_value: None,
349 name,
350 type_name: param.type_annotation.as_ref().map(format_type_annotation),
351 }
352 })
353 .collect();
354
355 DocItem {
356 kind: DocItemKind::Function,
357 name: path,
358 doc: doc.map(doc_text).unwrap_or_default(),
359 signature: Some(format_function_signature(func)),
360 type_params: format_type_params(&func.type_params),
361 params,
362 return_type: func.return_type.as_ref().map(format_type_annotation),
363 children: Vec::new(),
364 }
365}
366
367fn extract_struct_doc(
368 program: &Program,
369 path: String,
370 st: &shape_ast::ast::StructTypeDef,
371 span: Span,
372) -> DocItem {
373 let children = st
374 .fields
375 .iter()
376 .map(|field| DocItem {
377 kind: DocItemKind::Field,
378 name: join_child_path(&path, &field.name),
379 doc: doc_text_from_span(program, field.span),
380 signature: Some(format!(
381 "{}: {}",
382 field.name,
383 format_type_annotation(&field.type_annotation)
384 )),
385 type_params: Vec::new(),
386 params: Vec::new(),
387 return_type: Some(format_type_annotation(&field.type_annotation)),
388 children: Vec::new(),
389 })
390 .collect();
391
392 DocItem {
393 kind: DocItemKind::Type,
394 name: path,
395 doc: doc_text_from_span(program, span),
396 signature: None,
397 type_params: format_type_params(&st.type_params),
398 params: Vec::new(),
399 return_type: None,
400 children,
401 }
402}
403
404fn extract_enum_doc(
405 program: &Program,
406 path: String,
407 en: &shape_ast::ast::EnumDef,
408 span: Span,
409) -> DocItem {
410 let children = en
411 .members
412 .iter()
413 .map(|member| DocItem {
414 kind: DocItemKind::Variant,
415 name: join_child_path(&path, &member.name),
416 doc: doc_text_from_span(program, member.span),
417 signature: Some(match &member.kind {
418 shape_ast::ast::EnumMemberKind::Unit { .. } => member.name.clone(),
419 shape_ast::ast::EnumMemberKind::Tuple(items) => format!(
420 "{}({})",
421 member.name,
422 items
423 .iter()
424 .map(format_type_annotation)
425 .collect::<Vec<_>>()
426 .join(", ")
427 ),
428 shape_ast::ast::EnumMemberKind::Struct(fields) => format!(
429 "{} {{ {} }}",
430 member.name,
431 fields
432 .iter()
433 .map(|field| {
434 format!(
435 "{}: {}",
436 field.name,
437 format_type_annotation(&field.type_annotation)
438 )
439 })
440 .collect::<Vec<_>>()
441 .join(", ")
442 ),
443 }),
444 type_params: Vec::new(),
445 params: Vec::new(),
446 return_type: None,
447 children: Vec::new(),
448 })
449 .collect();
450
451 DocItem {
452 kind: DocItemKind::Enum,
453 name: path,
454 doc: doc_text_from_span(program, span),
455 signature: None,
456 type_params: format_type_params(&en.type_params),
457 params: Vec::new(),
458 return_type: None,
459 children,
460 }
461}
462
463fn extract_trait_doc(
464 program: &Program,
465 path: String,
466 tr: &shape_ast::ast::TraitDef,
467 span: Span,
468) -> DocItem {
469 let mut children = Vec::new();
470 for member in &tr.members {
471 match member {
472 TraitMember::Required(member) => {
473 children.push(extract_interface_member_doc(
474 program,
475 &path,
476 member,
477 DocItemKind::Method,
478 ));
479 }
480 TraitMember::Default(method) => {
481 children.push(DocItem {
482 kind: DocItemKind::Method,
483 name: join_child_path(&path, &method.name),
484 doc: doc_text_from_span(program, method.span),
485 signature: Some(format_method_signature(method)),
486 type_params: Vec::new(),
487 params: method
488 .params
489 .iter()
490 .map(|param| DocParam {
491 name: param.simple_name().unwrap_or("_").to_string(),
492 type_name: param.type_annotation.as_ref().map(format_type_annotation),
493 description: program
494 .docs
495 .comment_for_span(method.span)
496 .and_then(|doc| doc.param_doc(param.simple_name().unwrap_or("_")))
497 .map(str::to_string),
498 default_value: None,
499 })
500 .collect(),
501 return_type: method.return_type.as_ref().map(format_type_annotation),
502 children: Vec::new(),
503 });
504 }
505 TraitMember::AssociatedType { name, span, .. } => {
506 children.push(DocItem {
507 kind: DocItemKind::AssociatedType,
508 name: join_child_path(&path, name),
509 doc: doc_text_from_span(program, *span),
510 signature: Some(format!("type {}", name)),
511 type_params: Vec::new(),
512 params: Vec::new(),
513 return_type: None,
514 children: Vec::new(),
515 });
516 }
517 }
518 }
519
520 DocItem {
521 kind: DocItemKind::Trait,
522 name: path,
523 doc: doc_text_from_span(program, span),
524 signature: None,
525 type_params: format_type_params(&tr.type_params),
526 params: Vec::new(),
527 return_type: None,
528 children,
529 }
530}
531
532fn extract_interface_doc(
533 program: &Program,
534 path: String,
535 interface: &shape_ast::ast::InterfaceDef,
536 span: Span,
537) -> DocItem {
538 let children = interface
539 .members
540 .iter()
541 .map(|member| extract_interface_member_doc(program, &path, member, DocItemKind::Method))
542 .collect();
543
544 DocItem {
545 kind: DocItemKind::Interface,
546 name: path,
547 doc: doc_text_from_span(program, span),
548 signature: None,
549 type_params: format_type_params(&interface.type_params),
550 params: Vec::new(),
551 return_type: None,
552 children,
553 }
554}
555
556fn extract_interface_member_doc(
557 program: &Program,
558 parent_path: &str,
559 member: &InterfaceMember,
560 method_kind: DocItemKind,
561) -> DocItem {
562 match member {
563 InterfaceMember::Property {
564 name,
565 span,
566 type_annotation,
567 ..
568 } => DocItem {
569 kind: DocItemKind::Field,
570 name: join_child_path(parent_path, name),
571 doc: doc_text_from_span(program, *span),
572 signature: Some(format!(
573 "{}: {}",
574 name,
575 format_type_annotation(type_annotation)
576 )),
577 type_params: Vec::new(),
578 params: Vec::new(),
579 return_type: Some(format_type_annotation(type_annotation)),
580 children: Vec::new(),
581 },
582 InterfaceMember::Method {
583 name,
584 span,
585 params,
586 return_type,
587 ..
588 } => DocItem {
589 kind: method_kind,
590 name: join_child_path(parent_path, name),
591 doc: doc_text_from_span(program, *span),
592 signature: Some(format!(
593 "{}({}) -> {}",
594 name,
595 params
596 .iter()
597 .map(|param| {
598 let ty = format_type_annotation(¶m.type_annotation);
599 match ¶m.name {
600 Some(name) => format!("{}: {}", name, ty),
601 None => ty,
602 }
603 })
604 .collect::<Vec<_>>()
605 .join(", "),
606 format_type_annotation(return_type)
607 )),
608 type_params: Vec::new(),
609 params: params
610 .iter()
611 .map(|param| DocParam {
612 name: param.name.clone().unwrap_or_else(|| "_".to_string()),
613 type_name: Some(format_type_annotation(¶m.type_annotation)),
614 description: program
615 .docs
616 .comment_for_span(*span)
617 .and_then(|doc| doc.param_doc(param.name.as_deref().unwrap_or("_")))
618 .map(str::to_string),
619 default_value: None,
620 })
621 .collect(),
622 return_type: Some(format_type_annotation(return_type)),
623 children: Vec::new(),
624 },
625 InterfaceMember::IndexSignature {
626 span,
627 param_name,
628 param_type,
629 return_type,
630 ..
631 } => DocItem {
632 kind: method_kind,
633 name: join_child_path(parent_path, &format!("[{}]", param_type)),
634 doc: doc_text_from_span(program, *span),
635 signature: Some(format!(
636 "[{}: {}]: {}",
637 param_name,
638 param_type,
639 format_type_annotation(return_type)
640 )),
641 type_params: Vec::new(),
642 params: Vec::new(),
643 return_type: Some(format_type_annotation(return_type)),
644 children: Vec::new(),
645 },
646 }
647}
648
649fn doc_text_from_span(program: &Program, span: Span) -> String {
650 program
651 .docs
652 .comment_for_span(span)
653 .map(doc_text)
654 .unwrap_or_default()
655}
656
657fn doc_text(comment: &DocComment) -> String {
658 if !comment.body.is_empty() {
659 comment.body.clone()
660 } else {
661 comment.summary.clone()
662 }
663}
664
665fn format_type_params(type_params: &Option<Vec<shape_ast::ast::TypeParam>>) -> Vec<String> {
666 type_params
667 .as_ref()
668 .map(|params| params.iter().map(|tp| tp.name.clone()).collect())
669 .unwrap_or_default()
670}
671
672fn format_function_signature(func: &FunctionDef) -> String {
673 let type_params = format_type_params(&func.type_params);
674 let type_param_suffix = if type_params.is_empty() {
675 String::new()
676 } else {
677 format!("<{}>", type_params.join(", "))
678 };
679 let params = func
680 .params
681 .iter()
682 .map(|param| {
683 let name = param.simple_name().unwrap_or("_");
684 match ¶m.type_annotation {
685 Some(ty) => format!("{}: {}", name, format_type_annotation(ty)),
686 None => name.to_string(),
687 }
688 })
689 .collect::<Vec<_>>()
690 .join(", ");
691 let return_suffix = func
692 .return_type
693 .as_ref()
694 .map(|ty| format!(" -> {}", format_type_annotation(ty)))
695 .unwrap_or_default();
696 format!(
697 "fn {}{}({}){}",
698 func.name, type_param_suffix, params, return_suffix
699 )
700}
701
702fn format_method_signature(method: &shape_ast::ast::MethodDef) -> String {
703 let params = method
704 .params
705 .iter()
706 .map(|param| {
707 let name = param.simple_name().unwrap_or("_");
708 match ¶m.type_annotation {
709 Some(ty) => format!("{}: {}", name, format_type_annotation(ty)),
710 None => name.to_string(),
711 }
712 })
713 .collect::<Vec<_>>()
714 .join(", ");
715 let return_suffix = method
716 .return_type
717 .as_ref()
718 .map(|ty| format!(" -> {}", format_type_annotation(ty)))
719 .unwrap_or_default();
720 format!("fn {}({}){}", method.name, params, return_suffix)
721}
722
723fn format_builtin_signature(func: &shape_ast::ast::BuiltinFunctionDecl) -> String {
724 let params = func
725 .params
726 .iter()
727 .map(|param| {
728 let name = param.simple_name().unwrap_or("_");
729 let ty = param
730 .type_annotation
731 .as_ref()
732 .map(format_type_annotation)
733 .unwrap_or_else(|| "any".to_string());
734 format!("{}: {}", name, ty)
735 })
736 .collect::<Vec<_>>()
737 .join(", ");
738 let type_params = format_type_params(&func.type_params);
739 let type_param_suffix = if type_params.is_empty() {
740 String::new()
741 } else {
742 format!("<{}>", type_params.join(", "))
743 };
744 format!(
745 "{}{}({}) -> {}",
746 func.name,
747 type_param_suffix,
748 params,
749 format_type_annotation(&func.return_type)
750 )
751}
752
753fn format_foreign_signature(func: &shape_ast::ast::ForeignFunctionDef) -> String {
754 let params = func
755 .params
756 .iter()
757 .map(|param| {
758 let name = param.simple_name().unwrap_or("_");
759 match ¶m.type_annotation {
760 Some(ty) => format!("{}: {}", name, format_type_annotation(ty)),
761 None => name.to_string(),
762 }
763 })
764 .collect::<Vec<_>>()
765 .join(", ");
766 let type_params = format_type_params(&func.type_params);
767 let type_param_suffix = if type_params.is_empty() {
768 String::new()
769 } else {
770 format!("<{}>", type_params.join(", "))
771 };
772 let return_suffix = func
773 .return_type
774 .as_ref()
775 .map(|ty| format!(" -> {}", format_type_annotation(ty)))
776 .unwrap_or_default();
777 format!(
778 "fn {} {}{}({}){}",
779 func.language, func.name, type_param_suffix, params, return_suffix
780 )
781}
782
783fn format_type_annotation(ta: &TypeAnnotation) -> String {
784 match ta {
785 TypeAnnotation::Basic(name) => name.clone(),
786 TypeAnnotation::Array(inner) => format!("Array<{}>", format_type_annotation(inner)),
787 TypeAnnotation::Tuple(items) => {
788 let parts: Vec<String> = items.iter().map(format_type_annotation).collect();
789 format!("[{}]", parts.join(", "))
790 }
791 TypeAnnotation::Generic { name, args } => {
792 let parts: Vec<String> = args.iter().map(format_type_annotation).collect();
793 format!("{}<{}>", name, parts.join(", "))
794 }
795 TypeAnnotation::Reference(name) => name.to_string(),
796 TypeAnnotation::Void => "void".to_string(),
797 TypeAnnotation::Never => "never".to_string(),
798 TypeAnnotation::Null => "null".to_string(),
799 TypeAnnotation::Undefined => "undefined".to_string(),
800 TypeAnnotation::Dyn(bounds) => format!("dyn {}", bounds.join(" + ")),
801 TypeAnnotation::Function { params, returns } => {
802 let params = params
803 .iter()
804 .map(|param| match ¶m.name {
805 Some(name) => format!(
806 "{}: {}",
807 name,
808 format_type_annotation(¶m.type_annotation)
809 ),
810 None => format_type_annotation(¶m.type_annotation),
811 })
812 .collect::<Vec<_>>()
813 .join(", ");
814 format!("({}) => {}", params, format_type_annotation(returns))
815 }
816 TypeAnnotation::Union(items) => items
817 .iter()
818 .map(format_type_annotation)
819 .collect::<Vec<_>>()
820 .join(" | "),
821 TypeAnnotation::Intersection(items) => items
822 .iter()
823 .map(format_type_annotation)
824 .collect::<Vec<_>>()
825 .join(" + "),
826 TypeAnnotation::Object(fields) => format!(
827 "{{ {} }}",
828 fields
829 .iter()
830 .map(|field| format!(
831 "{}: {}",
832 field.name,
833 format_type_annotation(&field.type_annotation)
834 ))
835 .collect::<Vec<_>>()
836 .join(", ")
837 ),
838 }
839}
840
841fn join_path(prefix: &[String], name: &str) -> String {
842 if prefix.is_empty() {
843 name.to_string()
844 } else {
845 format!("{}::{}", prefix.join("::"), name)
846 }
847}
848
849fn join_child_path(parent: &str, name: &str) -> String {
850 format!("{}::{}", parent, name)
851}
852
853#[cfg(test)]
854mod tests {
855 use super::{DocItemKind, extract_docs_from_ast};
856
857 #[test]
858 fn extracts_function_docs_from_program_index() {
859 let source = "/// Doc for hello\n/// @param value input\nfn hello(value: string) -> string { value }";
860 let ast = shape_ast::parser::parse_program(source).expect("parse should succeed");
861 let docs = extract_docs_from_ast(source, &ast);
862 assert_eq!(docs.len(), 1);
863 assert_eq!(docs[0].kind, DocItemKind::Function);
864 assert_eq!(docs[0].doc, "Doc for hello");
865 assert_eq!(docs[0].params[0].description.as_deref(), Some("input"));
866 }
867
868 #[test]
869 fn extracts_child_docs_from_program_index() {
870 let source = "type Point {\n /// X coordinate\n x: number,\n}\n";
871 let ast = shape_ast::parser::parse_program(source).expect("parse should succeed");
872 let docs = extract_docs_from_ast(source, &ast);
873 assert_eq!(docs[0].children.len(), 1);
874 assert_eq!(docs[0].children[0].doc, "X coordinate");
875 }
876}