Skip to main content

shape_runtime/
doc_extract.rs

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