Skip to main content

shape_runtime/
doc_extract.rs

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(&param.type_annotation);
599                        match &param.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(&param.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 &param.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 &param.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 &param.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 &param.name {
805                    Some(name) => format!(
806                        "{}: {}",
807                        name,
808                        format_type_annotation(&param.type_annotation)
809                    ),
810                    None => format_type_annotation(&param.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}