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
669 .as_ref()
670 .map(|params| params.iter().map(|tp| tp.name().to_string()).collect())
671 .unwrap_or_default()
672}
673
674fn format_function_signature(func: &FunctionDef) -> String {
675 let type_params = format_type_params(&func.type_params);
676 let type_param_suffix = if type_params.is_empty() {
677 String::new()
678 } else {
679 format!("<{}>", type_params.join(", "))
680 };
681 let params = func
682 .params
683 .iter()
684 .map(|param| {
685 let name = param.simple_name().unwrap_or("_");
686 match ¶m.type_annotation {
687 Some(ty) => format!("{}: {}", name, format_type_annotation(ty)),
688 None => name.to_string(),
689 }
690 })
691 .collect::<Vec<_>>()
692 .join(", ");
693 let return_suffix = func
694 .return_type
695 .as_ref()
696 .map(|ty| format!(" -> {}", format_type_annotation(ty)))
697 .unwrap_or_default();
698 format!(
699 "fn {}{}({}){}",
700 func.name, type_param_suffix, params, return_suffix
701 )
702}
703
704fn format_method_signature(method: &shape_ast::ast::MethodDef) -> String {
705 let params = method
706 .params
707 .iter()
708 .map(|param| {
709 let name = param.simple_name().unwrap_or("_");
710 match ¶m.type_annotation {
711 Some(ty) => format!("{}: {}", name, format_type_annotation(ty)),
712 None => name.to_string(),
713 }
714 })
715 .collect::<Vec<_>>()
716 .join(", ");
717 let return_suffix = method
718 .return_type
719 .as_ref()
720 .map(|ty| format!(" -> {}", format_type_annotation(ty)))
721 .unwrap_or_default();
722 format!("fn {}({}){}", method.name, params, return_suffix)
723}
724
725fn format_builtin_signature(func: &shape_ast::ast::BuiltinFunctionDecl) -> String {
726 let params = func
727 .params
728 .iter()
729 .map(|param| {
730 let name = param.simple_name().unwrap_or("_");
731 let ty = param
732 .type_annotation
733 .as_ref()
734 .map(format_type_annotation)
735 .unwrap_or_else(|| "any".to_string());
736 format!("{}: {}", name, ty)
737 })
738 .collect::<Vec<_>>()
739 .join(", ");
740 let type_params = format_type_params(&func.type_params);
741 let type_param_suffix = if type_params.is_empty() {
742 String::new()
743 } else {
744 format!("<{}>", type_params.join(", "))
745 };
746 format!(
747 "{}{}({}) -> {}",
748 func.name,
749 type_param_suffix,
750 params,
751 format_type_annotation(&func.return_type)
752 )
753}
754
755fn format_foreign_signature(func: &shape_ast::ast::ForeignFunctionDef) -> String {
756 let params = func
757 .params
758 .iter()
759 .map(|param| {
760 let name = param.simple_name().unwrap_or("_");
761 match ¶m.type_annotation {
762 Some(ty) => format!("{}: {}", name, format_type_annotation(ty)),
763 None => name.to_string(),
764 }
765 })
766 .collect::<Vec<_>>()
767 .join(", ");
768 let type_params = format_type_params(&func.type_params);
769 let type_param_suffix = if type_params.is_empty() {
770 String::new()
771 } else {
772 format!("<{}>", type_params.join(", "))
773 };
774 let return_suffix = func
775 .return_type
776 .as_ref()
777 .map(|ty| format!(" -> {}", format_type_annotation(ty)))
778 .unwrap_or_default();
779 format!(
780 "fn {} {}{}({}){}",
781 func.language, func.name, type_param_suffix, params, return_suffix
782 )
783}
784
785fn format_type_annotation(ta: &TypeAnnotation) -> String {
786 match ta {
787 TypeAnnotation::Basic(name) => name.clone(),
788 TypeAnnotation::Array(inner) => format!("Array<{}>", format_type_annotation(inner)),
789 TypeAnnotation::Tuple(items) => {
790 let parts: Vec<String> = items.iter().map(format_type_annotation).collect();
791 format!("[{}]", parts.join(", "))
792 }
793 TypeAnnotation::Generic { name, args } => {
794 let parts: Vec<String> = args.iter().map(format_type_annotation).collect();
795 format!("{}<{}>", name, parts.join(", "))
796 }
797 TypeAnnotation::Reference(name) => name.to_string(),
798 TypeAnnotation::Void => "void".to_string(),
799 TypeAnnotation::Never => "never".to_string(),
800 TypeAnnotation::Null => "null".to_string(),
801 TypeAnnotation::Undefined => "undefined".to_string(),
802 TypeAnnotation::Dyn(bounds) => format!("dyn {}", bounds.join(" + ")),
803 TypeAnnotation::Function { params, returns } => {
804 let params = params
805 .iter()
806 .map(|param| match ¶m.name {
807 Some(name) => format!(
808 "{}: {}",
809 name,
810 format_type_annotation(¶m.type_annotation)
811 ),
812 None => format_type_annotation(¶m.type_annotation),
813 })
814 .collect::<Vec<_>>()
815 .join(", ");
816 format!("({}) => {}", params, format_type_annotation(returns))
817 }
818 TypeAnnotation::Union(items) => items
819 .iter()
820 .map(format_type_annotation)
821 .collect::<Vec<_>>()
822 .join(" | "),
823 TypeAnnotation::Intersection(items) => items
824 .iter()
825 .map(format_type_annotation)
826 .collect::<Vec<_>>()
827 .join(" + "),
828 TypeAnnotation::Object(fields) => format!(
829 "{{ {} }}",
830 fields
831 .iter()
832 .map(|field| format!(
833 "{}: {}",
834 field.name,
835 format_type_annotation(&field.type_annotation)
836 ))
837 .collect::<Vec<_>>()
838 .join(", ")
839 ),
840 }
841}
842
843fn join_path(prefix: &[String], name: &str) -> String {
844 if prefix.is_empty() {
845 name.to_string()
846 } else {
847 format!("{}::{}", prefix.join("::"), name)
848 }
849}
850
851fn join_child_path(parent: &str, name: &str) -> String {
852 format!("{}::{}", parent, name)
853}
854
855#[cfg(test)]
856mod tests {
857 use super::{DocItemKind, extract_docs_from_ast};
858
859 #[test]
860 fn extracts_function_docs_from_program_index() {
861 let source = "/// Doc for hello\n/// @param value input\nfn hello(value: string) -> string { value }";
862 let ast = shape_ast::parser::parse_program(source).expect("parse should succeed");
863 let docs = extract_docs_from_ast(source, &ast);
864 assert_eq!(docs.len(), 1);
865 assert_eq!(docs[0].kind, DocItemKind::Function);
866 assert_eq!(docs[0].doc, "Doc for hello");
867 assert_eq!(docs[0].params[0].description.as_deref(), Some("input"));
868 }
869
870 #[test]
871 fn extracts_child_docs_from_program_index() {
872 let source = "type Point {\n /// X coordinate\n x: number,\n}\n";
873 let ast = shape_ast::parser::parse_program(source).expect("parse should succeed");
874 let docs = extract_docs_from_ast(source, &ast);
875 assert_eq!(docs[0].children.len(), 1);
876 assert_eq!(docs[0].children[0].doc, "X coordinate");
877 }
878}