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