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(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(&param.type_annotation);
530                        match &param.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(&param.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 &param.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 &param.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 &param.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 &param.name {
735                    Some(name) => format!("{}: {}", name, format_type_annotation(&param.type_annotation)),
736                    None => format_type_annotation(&param.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}