1use crate::ast::{
2 DocComment, DocEntry, DocLink, DocTag, DocTagKind, DocTarget, DocTargetKind, ExportItem, Item,
3 Program, ProgramDocs, Span, TraitMember, TypeParam, extend_method_doc_path,
4 impl_method_doc_path,
5};
6use pest::iterators::Pair;
7
8use super::Rule;
9
10pub fn parse_doc_comment(pair: Pair<Rule>) -> DocComment {
11 debug_assert!(matches!(
12 pair.as_rule(),
13 Rule::doc_comment | Rule::program_doc_comment
14 ));
15 let span = crate::parser::pair_span(&pair);
16 let is_program_doc = pair.as_rule() == Rule::program_doc_comment;
17
18 let lines = pair
19 .into_inner()
20 .filter(|line| {
21 matches!(
22 line.as_rule(),
23 Rule::doc_comment_line | Rule::program_doc_comment_head
24 )
25 })
26 .map(parse_doc_line)
27 .collect::<Vec<_>>();
28
29 if is_program_doc {
30 parse_program_doc_lines(span, &lines)
31 } else {
32 parse_doc_lines(span, &lines)
33 }
34}
35
36pub fn build_program_docs(
37 program: &Program,
38 module_doc_comment: Option<&DocComment>,
39) -> ProgramDocs {
40 let mut collector = DocCollector::default();
41 collector.collect_program_doc(module_doc_comment);
42 collector.collect_items(&program.items, &[]);
43 ProgramDocs {
44 entries: collector.entries,
45 }
46}
47
48#[derive(Debug, Clone)]
49struct DocLine {
50 text: String,
51 span: Span,
52}
53
54fn parse_doc_line(line: Pair<Rule>) -> DocLine {
55 let raw = line.as_str();
56 let raw_span = crate::parser::pair_span(&line);
57 let rest = raw
58 .strip_prefix("///")
59 .expect("doc comment lines must start with ///");
60 let prefix_len = if rest.starts_with(' ') { 4 } else { 3 };
61 let text = rest.strip_prefix(' ').unwrap_or(rest).to_string();
62 let content_start = (raw_span.start + prefix_len).min(raw_span.end);
63 DocLine {
64 text,
65 span: Span::new(content_start, raw_span.end),
66 }
67}
68
69fn parse_program_doc_lines(span: Span, lines: &[DocLine]) -> DocComment {
70 let Some((first_line, remaining_lines)) = lines.split_first() else {
71 return DocComment::default();
72 };
73 let Some(module_tag) = parse_tag_line(first_line) else {
74 return parse_doc_lines(span, lines);
75 };
76
77 let mut comment = parse_doc_lines(span, remaining_lines);
78 comment.span = span;
79 comment.tags.insert(0, module_tag);
80 comment
81}
82
83fn parse_doc_lines(span: Span, lines: &[DocLine]) -> DocComment {
84 let mut body_lines = Vec::new();
85 let mut tags = Vec::new();
86 let mut current_tag: Option<DocTag> = None;
87
88 for line in lines {
89 let trimmed = line.text.trim_end();
90 if let Some(parsed_tag) = parse_tag_line(line) {
91 if let Some(tag) = current_tag.take() {
92 tags.push(tag);
93 }
94 current_tag = Some(parsed_tag);
95 continue;
96 }
97
98 if let Some(tag) = current_tag.as_mut() {
99 if !tag.body.is_empty() {
100 tag.body.push('\n');
101 }
102 tag.body.push_str(trimmed);
103 tag.span = tag.span.merge(line.span);
104 tag.body_span = Some(match tag.body_span {
105 Some(body_span) => body_span.merge(line.span),
106 None => line.span,
107 });
108 } else {
109 body_lines.push(trimmed.to_string());
110 }
111 }
112
113 if let Some(tag) = current_tag.take() {
114 tags.push(tag);
115 }
116
117 let body = body_lines.join("\n").trim().to_string();
118 let summary = body
119 .lines()
120 .find(|line| !line.trim().is_empty())
121 .map(|line| line.trim().to_string())
122 .unwrap_or_default();
123
124 DocComment {
125 span,
126 summary,
127 body,
128 tags,
129 }
130}
131
132fn parse_tag_line(line: &DocLine) -> Option<DocTag> {
133 let leading = trim_start_offset(&line.text);
134 if line.text[leading..].chars().next()? != '@' {
135 return None;
136 }
137
138 let tag_name_start = leading + 1;
139 let tag_name_end = token_end_offset(&line.text, tag_name_start);
140 let remainder_start = skip_whitespace_offset(&line.text, tag_name_end);
141 let tag_name = &line.text[tag_name_start..tag_name_end];
142 let remainder = &line.text[remainder_start..];
143 let kind = match tag_name {
144 "module" => DocTagKind::Module,
145 "typeparam" => DocTagKind::TypeParam,
146 "param" => DocTagKind::Param,
147 "returns" => DocTagKind::Returns,
148 "throws" => DocTagKind::Throws,
149 "deprecated" => DocTagKind::Deprecated,
150 "requires" => DocTagKind::Requires,
151 "since" => DocTagKind::Since,
152 "see" => DocTagKind::See,
153 "link" => DocTagKind::Link,
154 "note" => DocTagKind::Note,
155 "example" => DocTagKind::Example,
156 other => DocTagKind::Unknown(other.to_string()),
157 };
158
159 let tag_span = span_from_offsets(line.span, leading, line.text.len());
160 let kind_span = span_from_offsets(line.span, tag_name_start, tag_name_end);
161
162 Some(match kind {
163 DocTagKind::TypeParam | DocTagKind::Param => {
164 let name_start = remainder_start;
165 let name_end = token_end_offset(&line.text, name_start);
166 let body_start = skip_whitespace_offset(&line.text, name_end);
167 let name = &line.text[name_start..name_end];
168 let body = &line.text[body_start..];
169 DocTag {
170 kind,
171 span: tag_span,
172 kind_span,
173 name: (!name.is_empty()).then(|| name.to_string()),
174 name_span: (!name.is_empty())
175 .then(|| span_from_offsets(line.span, name_start, name_end)),
176 body: body.trim().to_string(),
177 body_span: (!body.trim().is_empty())
178 .then(|| span_from_offsets(line.span, body_start, line.text.len())),
179 link: None,
180 }
181 }
182 DocTagKind::See => DocTag {
183 kind,
184 span: tag_span,
185 kind_span,
186 name: None,
187 name_span: None,
188 body: remainder.trim().to_string(),
189 body_span: (!remainder.trim().is_empty())
190 .then(|| span_from_offsets(line.span, remainder_start, line.text.len())),
191 link: parse_link(line, remainder_start, false),
192 },
193 DocTagKind::Link => DocTag {
194 kind,
195 span: tag_span,
196 kind_span,
197 name: None,
198 name_span: None,
199 body: remainder.trim().to_string(),
200 body_span: (!remainder.trim().is_empty())
201 .then(|| span_from_offsets(line.span, remainder_start, line.text.len())),
202 link: parse_link(line, remainder_start, true),
203 },
204 _ => DocTag {
205 kind,
206 span: tag_span,
207 kind_span,
208 name: None,
209 name_span: None,
210 body: remainder.trim().to_string(),
211 body_span: (!remainder.trim().is_empty())
212 .then(|| span_from_offsets(line.span, remainder_start, line.text.len())),
213 link: None,
214 },
215 })
216}
217
218fn parse_link(line: &DocLine, start_offset: usize, allow_label: bool) -> Option<DocLink> {
219 let trimmed_start = skip_whitespace_offset(&line.text, start_offset);
220 let trimmed = &line.text[trimmed_start..];
221 if trimmed.is_empty() {
222 return None;
223 }
224 let target_end = token_end_offset(&line.text, trimmed_start);
225 let target = &line.text[trimmed_start..target_end];
226 let label_start = skip_whitespace_offset(&line.text, target_end);
227 let label = allow_label.then(|| line.text[label_start..].trim().to_string());
228 let label = label.filter(|value| !value.is_empty());
229 let label_span = label
230 .as_ref()
231 .map(|_| span_from_offsets(line.span, label_start, line.text.len()));
232 Some(DocLink {
233 target: target.to_string(),
234 target_span: span_from_offsets(line.span, trimmed_start, target_end),
235 label,
236 label_span,
237 })
238}
239
240fn trim_start_offset(text: &str) -> usize {
241 text.char_indices()
242 .find(|(_, ch)| !ch.is_whitespace())
243 .map(|(idx, _)| idx)
244 .unwrap_or(text.len())
245}
246
247fn skip_whitespace_offset(text: &str, start: usize) -> usize {
248 let tail = &text[start.min(text.len())..];
249 start
250 + tail
251 .char_indices()
252 .find(|(_, ch)| !ch.is_whitespace())
253 .map(|(idx, _)| idx)
254 .unwrap_or(tail.len())
255}
256
257fn token_end_offset(text: &str, start: usize) -> usize {
258 let tail = &text[start.min(text.len())..];
259 start
260 + tail
261 .char_indices()
262 .find(|(_, ch)| ch.is_whitespace())
263 .map(|(idx, _)| idx)
264 .unwrap_or(tail.len())
265}
266
267fn span_from_offsets(base: Span, start: usize, end: usize) -> Span {
268 Span::new(base.start + start, (base.start + end).min(base.end))
269}
270
271#[derive(Default)]
272struct DocCollector {
273 entries: Vec<DocEntry>,
274}
275
276impl DocCollector {
277 fn collect_program_doc(&mut self, doc_comment: Option<&DocComment>) {
278 let Some(comment) = doc_comment else {
279 return;
280 };
281 let Some(path) = module_path_from_comment(comment) else {
282 return;
283 };
284 self.entries.push(DocEntry {
285 target: DocTarget {
286 kind: DocTargetKind::Module,
287 path,
288 span: comment.span,
289 },
290 comment: comment.clone(),
291 });
292 }
293
294 fn collect_items(&mut self, items: &[Item], module_path: &[String]) {
295 for item in items {
296 self.collect_item(item, module_path);
297 }
298 }
299
300 fn collect_item(&mut self, item: &Item, module_path: &[String]) {
301 match item {
302 Item::Module(module, span) => {
303 let path = join_path(module_path, &module.name);
304 self.attach_comment(
305 DocTargetKind::Module,
306 path.clone(),
307 *span,
308 module.doc_comment.as_ref(),
309 );
310 self.collect_items(&module.items, &append_path(module_path, &module.name));
311 }
312 Item::Function(function, span) => {
313 let path = join_path(module_path, &function.name);
314 self.attach_comment(
315 DocTargetKind::Function,
316 path.clone(),
317 *span,
318 function.doc_comment.as_ref(),
319 );
320 self.collect_type_params(&path, function.type_params.as_deref());
321 }
322 Item::AnnotationDef(annotation_def, span) => {
323 let path = join_annotation_path(module_path, &annotation_def.name);
324 self.attach_comment(
325 DocTargetKind::Annotation,
326 path.clone(),
327 *span,
328 annotation_def.doc_comment.as_ref(),
329 );
330 }
331 Item::ForeignFunction(function, span) => {
332 let path = join_path(module_path, &function.name);
333 self.attach_comment(
334 DocTargetKind::ForeignFunction,
335 path.clone(),
336 *span,
337 function.doc_comment.as_ref(),
338 );
339 self.collect_type_params(&path, function.type_params.as_deref());
340 }
341 Item::BuiltinFunctionDecl(function, span) => {
342 let path = join_path(module_path, &function.name);
343 self.attach_comment(
344 DocTargetKind::BuiltinFunction,
345 path.clone(),
346 *span,
347 function.doc_comment.as_ref(),
348 );
349 self.collect_type_params(&path, function.type_params.as_deref());
350 }
351 Item::BuiltinTypeDecl(ty, span) => {
352 let path = join_path(module_path, &ty.name);
353 self.attach_comment(
354 DocTargetKind::BuiltinType,
355 path.clone(),
356 *span,
357 ty.doc_comment.as_ref(),
358 );
359 self.collect_type_params(&path, ty.type_params.as_deref());
360 }
361 Item::TypeAlias(alias, span) => {
362 let path = join_path(module_path, &alias.name);
363 self.attach_comment(
364 DocTargetKind::TypeAlias,
365 path.clone(),
366 *span,
367 alias.doc_comment.as_ref(),
368 );
369 self.collect_type_params(&path, alias.type_params.as_deref());
370 }
371 Item::StructType(struct_def, span) => {
372 let path = join_path(module_path, &struct_def.name);
373 self.collect_struct(&path, *span, struct_def.doc_comment.as_ref(), struct_def);
374 }
375 Item::Enum(enum_def, span) => {
376 let path = join_path(module_path, &enum_def.name);
377 self.collect_enum(&path, *span, enum_def.doc_comment.as_ref(), enum_def);
378 }
379 Item::Interface(interface_def, span) => {
380 let path = join_path(module_path, &interface_def.name);
381 self.collect_interface(
382 &path,
383 *span,
384 interface_def.doc_comment.as_ref(),
385 interface_def,
386 );
387 }
388 Item::Trait(trait_def, span) => {
389 let path = join_path(module_path, &trait_def.name);
390 self.collect_trait(&path, *span, trait_def.doc_comment.as_ref(), trait_def);
391 }
392 Item::Extend(extend, span) => {
393 self.collect_extend(module_path, *span, extend);
394 }
395 Item::Impl(impl_block, span) => {
396 self.collect_impl(module_path, *span, impl_block);
397 }
398 Item::Export(export, span) => match &export.item {
399 ExportItem::Function(function) => {
400 let path = join_path(module_path, &function.name);
401 self.attach_comment(
402 DocTargetKind::Function,
403 path.clone(),
404 *span,
405 function.doc_comment.as_ref(),
406 );
407 self.collect_type_params(&path, function.type_params.as_deref());
408 }
409 ExportItem::BuiltinFunction(function) => {
410 let path = join_path(module_path, &function.name);
411 self.attach_comment(
412 DocTargetKind::Function,
413 path.clone(),
414 *span,
415 function.doc_comment.as_ref(),
416 );
417 self.collect_type_params(&path, function.type_params.as_deref());
418 }
419 ExportItem::BuiltinType(ty) => {
420 let path = join_path(module_path, &ty.name);
421 self.attach_comment(
422 DocTargetKind::TypeAlias,
423 path.clone(),
424 *span,
425 ty.doc_comment.as_ref(),
426 );
427 self.collect_type_params(&path, ty.type_params.as_deref());
428 }
429 ExportItem::ForeignFunction(function) => {
430 let path = join_path(module_path, &function.name);
431 self.attach_comment(
432 DocTargetKind::ForeignFunction,
433 path.clone(),
434 *span,
435 function.doc_comment.as_ref(),
436 );
437 self.collect_type_params(&path, function.type_params.as_deref());
438 }
439 ExportItem::TypeAlias(alias) => {
440 let path = join_path(module_path, &alias.name);
441 self.attach_comment(
442 DocTargetKind::TypeAlias,
443 path.clone(),
444 *span,
445 alias.doc_comment.as_ref(),
446 );
447 self.collect_type_params(&path, alias.type_params.as_deref());
448 }
449 ExportItem::Struct(struct_def) => {
450 let path = join_path(module_path, &struct_def.name);
451 self.collect_struct(&path, *span, struct_def.doc_comment.as_ref(), struct_def);
452 }
453 ExportItem::Enum(enum_def) => {
454 let path = join_path(module_path, &enum_def.name);
455 self.collect_enum(&path, *span, enum_def.doc_comment.as_ref(), enum_def);
456 }
457 ExportItem::Interface(interface_def) => {
458 let path = join_path(module_path, &interface_def.name);
459 self.collect_interface(
460 &path,
461 *span,
462 interface_def.doc_comment.as_ref(),
463 interface_def,
464 );
465 }
466 ExportItem::Trait(trait_def) => {
467 let path = join_path(module_path, &trait_def.name);
468 self.collect_trait(&path, *span, trait_def.doc_comment.as_ref(), trait_def);
469 }
470 ExportItem::Annotation(annotation_def) => {
471 let path = join_path(module_path, &annotation_def.name);
472 self.attach_comment(
473 DocTargetKind::Annotation,
474 path,
475 *span,
476 annotation_def.doc_comment.as_ref(),
477 );
478 }
479 ExportItem::Named(_) => {}
480 },
481 _ => {}
482 }
483 }
484
485 fn collect_struct(
486 &mut self,
487 path: &str,
488 span: Span,
489 doc_comment: Option<&DocComment>,
490 struct_def: &crate::ast::StructTypeDef,
491 ) {
492 self.attach_comment(DocTargetKind::Struct, path.to_string(), span, doc_comment);
493 self.collect_type_params(path, struct_def.type_params.as_deref());
494 for field in &struct_def.fields {
495 self.attach_comment(
496 DocTargetKind::StructField,
497 join_child_path(path, &field.name),
498 field.span,
499 field.doc_comment.as_ref(),
500 );
501 }
502 }
503
504 fn collect_enum(
505 &mut self,
506 path: &str,
507 span: Span,
508 doc_comment: Option<&DocComment>,
509 enum_def: &crate::ast::EnumDef,
510 ) {
511 self.attach_comment(DocTargetKind::Enum, path.to_string(), span, doc_comment);
512 self.collect_type_params(path, enum_def.type_params.as_deref());
513 for member in &enum_def.members {
514 self.attach_comment(
515 DocTargetKind::EnumVariant,
516 join_child_path(path, &member.name),
517 member.span,
518 member.doc_comment.as_ref(),
519 );
520 }
521 }
522
523 fn collect_interface(
524 &mut self,
525 path: &str,
526 span: Span,
527 doc_comment: Option<&DocComment>,
528 interface_def: &crate::ast::InterfaceDef,
529 ) {
530 self.attach_comment(
531 DocTargetKind::Interface,
532 path.to_string(),
533 span,
534 doc_comment,
535 );
536 self.collect_type_params(path, interface_def.type_params.as_deref());
537 for member in &interface_def.members {
538 let (kind, name) = match member {
539 crate::ast::InterfaceMember::Property { name, .. } => {
540 (DocTargetKind::InterfaceProperty, name.as_str())
541 }
542 crate::ast::InterfaceMember::Method { name, .. } => {
543 (DocTargetKind::InterfaceMethod, name.as_str())
544 }
545 crate::ast::InterfaceMember::IndexSignature { param_type, .. } => {
546 (DocTargetKind::InterfaceIndexSignature, param_type.as_str())
547 }
548 };
549 let child_name = if matches!(kind, DocTargetKind::InterfaceIndexSignature) {
550 format!("[{}]", name)
551 } else {
552 name.to_string()
553 };
554 self.attach_comment(
555 kind,
556 join_child_path(path, &child_name),
557 member.span(),
558 member.doc_comment(),
559 );
560 }
561 }
562
563 fn collect_trait(
564 &mut self,
565 path: &str,
566 span: Span,
567 doc_comment: Option<&DocComment>,
568 trait_def: &crate::ast::TraitDef,
569 ) {
570 self.attach_comment(DocTargetKind::Trait, path.to_string(), span, doc_comment);
571 self.collect_type_params(path, trait_def.type_params.as_deref());
572 for member in &trait_def.members {
573 let (kind, child_name, child_span) = match member {
574 TraitMember::Required(crate::ast::InterfaceMember::Property {
575 name, span, ..
576 })
577 | TraitMember::Required(crate::ast::InterfaceMember::Method {
578 name, span, ..
579 }) => (DocTargetKind::TraitMethod, name.clone(), *span),
580 TraitMember::Required(crate::ast::InterfaceMember::IndexSignature {
581 param_type,
582 span,
583 ..
584 }) => (
585 DocTargetKind::TraitMethod,
586 format!("[{}]", param_type),
587 *span,
588 ),
589 TraitMember::Default(method) => {
590 (DocTargetKind::TraitMethod, method.name.clone(), method.span)
591 }
592 TraitMember::AssociatedType { name, span, .. } => {
593 (DocTargetKind::TraitAssociatedType, name.clone(), *span)
594 }
595 };
596 self.attach_comment(
597 kind,
598 join_child_path(path, &child_name),
599 child_span,
600 member.doc_comment(),
601 );
602 }
603 }
604
605 fn collect_extend(
606 &mut self,
607 module_path: &[String],
608 _span: Span,
609 extend: &crate::ast::ExtendStatement,
610 ) {
611 for method in &extend.methods {
612 self.attach_comment(
613 DocTargetKind::ExtensionMethod,
614 extend_method_doc_path(module_path, &extend.type_name, &method.name),
615 method.span,
616 method.doc_comment.as_ref(),
617 );
618 }
619 }
620
621 fn collect_impl(
622 &mut self,
623 module_path: &[String],
624 _span: Span,
625 impl_block: &crate::ast::ImplBlock,
626 ) {
627 for method in &impl_block.methods {
628 self.attach_comment(
629 DocTargetKind::ImplMethod,
630 impl_method_doc_path(
631 module_path,
632 &impl_block.trait_name,
633 &impl_block.target_type,
634 &method.name,
635 ),
636 method.span,
637 method.doc_comment.as_ref(),
638 );
639 }
640 }
641
642 fn collect_type_params(&mut self, parent_path: &str, type_params: Option<&[TypeParam]>) {
643 for type_param in type_params.unwrap_or(&[]) {
644 self.attach_comment(
645 DocTargetKind::TypeParam,
646 join_type_param_path(parent_path, &type_param.name),
647 type_param.span,
648 type_param.doc_comment.as_ref(),
649 );
650 }
651 }
652
653 fn attach_comment(
654 &mut self,
655 kind: DocTargetKind,
656 path: String,
657 span: Span,
658 doc_comment: Option<&DocComment>,
659 ) {
660 let Some(comment) = doc_comment.cloned() else {
661 return;
662 };
663 self.entries.push(DocEntry {
664 target: DocTarget { kind, path, span },
665 comment,
666 });
667 }
668}
669
670fn append_path(module_path: &[String], name: &str) -> Vec<String> {
671 let mut next = module_path.to_vec();
672 next.push(name.to_string());
673 next
674}
675
676fn join_path(prefix: &[String], name: &str) -> String {
677 if prefix.is_empty() {
678 name.to_string()
679 } else {
680 format!("{}::{}", prefix.join("::"), name)
681 }
682}
683
684fn join_annotation_path(prefix: &[String], name: &str) -> String {
685 join_path(prefix, &format!("@{name}"))
686}
687
688fn join_child_path(parent: &str, name: &str) -> String {
689 format!("{}::{}", parent, name)
690}
691
692fn join_type_param_path(parent: &str, name: &str) -> String {
693 format!("{}::<{}>", parent, name)
694}
695
696fn module_path_from_comment(comment: &DocComment) -> Option<String> {
697 comment.tags.iter().find_map(|tag| match tag.kind {
698 DocTagKind::Module if !tag.body.trim().is_empty() => Some(tag.body.trim().to_string()),
699 _ => None,
700 })
701}
702
703#[cfg(test)]
704mod tests {
705 use crate::ast::{DocTargetKind, Item};
706 use crate::parser::parse_program;
707
708 #[test]
709 fn attaches_docs_to_top_level_items() {
710 let program = parse_program("/// Adds\nfn add(x: number) -> number { x }\n")
711 .expect("program should parse");
712 let doc = program
713 .docs
714 .comment_for_path("add")
715 .expect("doc for function");
716 assert_eq!(doc.summary, "Adds");
717 }
718
719 #[test]
720 fn attaches_docs_to_program_modules() {
721 let source =
722 "/// @module std::core::json_value\n/// Typed JSON values.\npub enum Json { Null }\n";
723 let program = parse_program(source).expect("program should parse");
724 let entry = program
725 .docs
726 .entry_for_path("std::core::json_value")
727 .expect("doc entry for module");
728 assert_eq!(entry.target.kind, DocTargetKind::Module);
729 assert_eq!(entry.comment.summary, "Typed JSON values.");
730 }
731
732 #[test]
733 fn attaches_docs_to_struct_members() {
734 let source = "type Point {\n /// X coordinate\n x: number,\n}\n";
735 let program = parse_program(source).expect("program should parse");
736 let doc = program
737 .docs
738 .comment_for_path("Point::x")
739 .expect("doc for field");
740 assert_eq!(doc.summary, "X coordinate");
741 }
742
743 #[test]
744 fn attaches_docs_to_type_params() {
745 let source = "fn identity<\n /// Input type\n T\n>(value: T) -> T { value }\n";
746 let program = parse_program(source).expect("program should parse");
747 let entry = program
748 .docs
749 .entry_for_path("identity::<T>")
750 .expect("doc for type param");
751 assert_eq!(entry.target.kind, DocTargetKind::TypeParam);
752 assert_eq!(entry.comment.summary, "Input type");
753 }
754
755 #[test]
756 fn parses_structured_tags() {
757 let source = "/// Summary\n/// @param x value\nfn add(x: number) -> number { x }\n";
758 let program = parse_program(source).expect("program should parse");
759 let doc = program
760 .docs
761 .comment_for_path("add")
762 .expect("doc for function");
763 assert_eq!(doc.param_doc("x"), Some("value"));
764 }
765
766 #[test]
767 fn attaches_docs_to_annotation_defs() {
768 let source = "/// Configures warmup handling.\n/// @param period Number of lookback bars.\nannotation warmup(period) { metadata() { return { warmup: period } } }\n";
769 let program = parse_program(source).expect("program should parse");
770 let entry = program
771 .docs
772 .entry_for_path("@warmup")
773 .expect("doc for annotation");
774 assert_eq!(entry.target.kind, DocTargetKind::Annotation);
775 assert_eq!(
776 entry.comment.param_doc("period"),
777 Some("Number of lookback bars.")
778 );
779 }
780
781 #[test]
782 fn block_doc_comments_do_not_create_docs() {
783 let source = "/** Old style */\nfn add(x: number) -> number { x }\n";
784 let program = parse_program(source).expect("program should parse");
785 assert!(program.docs.comment_for_path("add").is_none());
786 }
787
788 #[test]
789 fn attaches_docs_to_extend_methods() {
790 let source = "extend Json {\n /// Access a field.\n method get(key: string) -> Json { self }\n}\n";
791 let program = parse_program(source).expect("program should parse");
792 let extend = program
793 .items
794 .iter()
795 .find_map(|item| match item {
796 Item::Extend(extend, _) => Some(extend),
797 _ => None,
798 })
799 .expect("extend block");
800 let method = extend.methods.first().expect("extend method");
801 let entry = program
802 .docs
803 .entry_for_span(method.span)
804 .expect("doc entry for extend method");
805 assert_eq!(entry.target.kind, DocTargetKind::ExtensionMethod);
806 assert_eq!(entry.comment.summary, "Access a field.");
807 }
808
809 #[test]
810 fn attaches_docs_to_impl_methods() {
811 let source = "impl Display for Json {\n /// Render the value.\n method render() -> string { \"json\" }\n}\n";
812 let program = parse_program(source).expect("program should parse");
813 let impl_block = program
814 .items
815 .iter()
816 .find_map(|item| match item {
817 Item::Impl(impl_block, _) => Some(impl_block),
818 _ => None,
819 })
820 .expect("impl block");
821 let method = impl_block.methods.first().expect("impl method");
822 let entry = program
823 .docs
824 .entry_for_span(method.span)
825 .expect("doc entry for impl method");
826 assert_eq!(entry.target.kind, DocTargetKind::ImplMethod);
827 assert_eq!(entry.comment.summary, "Render the value.");
828 }
829
830 #[test]
831 fn parses_stdlib_json_value_module_with_documented_methods() {
832 let source = include_str!("../../../shape-runtime/stdlib-src/core/json_value.shape");
833 let program = parse_program(source).expect("stdlib json_value module should parse");
834 assert!(
835 program
836 .docs
837 .entry_for_path("std::core::json_value")
838 .is_some()
839 );
840 assert!(
841 program
842 .docs
843 .entries
844 .iter()
845 .any(|entry| entry.target.kind == DocTargetKind::ExtensionMethod),
846 "expected documented extension methods in std::core::json_value"
847 );
848 }
849}