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(¶m.type_annotation);
558 match ¶m.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(¶m.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 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 ¶m.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 ¶m.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 ¶m.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 ¶m.name {
766 Some(name) => format!(
767 "{}: {}",
768 name,
769 format_type_annotation(¶m.type_annotation)
770 ),
771 None => format_type_annotation(¶m.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}