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::ForeignFunction(function) => {
410 let path = join_path(module_path, &function.name);
411 self.attach_comment(
412 DocTargetKind::ForeignFunction,
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::TypeAlias(alias) => {
420 let path = join_path(module_path, &alias.name);
421 self.attach_comment(
422 DocTargetKind::TypeAlias,
423 path.clone(),
424 *span,
425 alias.doc_comment.as_ref(),
426 );
427 self.collect_type_params(&path, alias.type_params.as_deref());
428 }
429 ExportItem::Struct(struct_def) => {
430 let path = join_path(module_path, &struct_def.name);
431 self.collect_struct(&path, *span, struct_def.doc_comment.as_ref(), struct_def);
432 }
433 ExportItem::Enum(enum_def) => {
434 let path = join_path(module_path, &enum_def.name);
435 self.collect_enum(&path, *span, enum_def.doc_comment.as_ref(), enum_def);
436 }
437 ExportItem::Interface(interface_def) => {
438 let path = join_path(module_path, &interface_def.name);
439 self.collect_interface(
440 &path,
441 *span,
442 interface_def.doc_comment.as_ref(),
443 interface_def,
444 );
445 }
446 ExportItem::Trait(trait_def) => {
447 let path = join_path(module_path, &trait_def.name);
448 self.collect_trait(&path, *span, trait_def.doc_comment.as_ref(), trait_def);
449 }
450 ExportItem::Named(_) => {}
451 },
452 _ => {}
453 }
454 }
455
456 fn collect_struct(
457 &mut self,
458 path: &str,
459 span: Span,
460 doc_comment: Option<&DocComment>,
461 struct_def: &crate::ast::StructTypeDef,
462 ) {
463 self.attach_comment(DocTargetKind::Struct, path.to_string(), span, doc_comment);
464 self.collect_type_params(path, struct_def.type_params.as_deref());
465 for field in &struct_def.fields {
466 self.attach_comment(
467 DocTargetKind::StructField,
468 join_child_path(path, &field.name),
469 field.span,
470 field.doc_comment.as_ref(),
471 );
472 }
473 }
474
475 fn collect_enum(
476 &mut self,
477 path: &str,
478 span: Span,
479 doc_comment: Option<&DocComment>,
480 enum_def: &crate::ast::EnumDef,
481 ) {
482 self.attach_comment(DocTargetKind::Enum, path.to_string(), span, doc_comment);
483 self.collect_type_params(path, enum_def.type_params.as_deref());
484 for member in &enum_def.members {
485 self.attach_comment(
486 DocTargetKind::EnumVariant,
487 join_child_path(path, &member.name),
488 member.span,
489 member.doc_comment.as_ref(),
490 );
491 }
492 }
493
494 fn collect_interface(
495 &mut self,
496 path: &str,
497 span: Span,
498 doc_comment: Option<&DocComment>,
499 interface_def: &crate::ast::InterfaceDef,
500 ) {
501 self.attach_comment(
502 DocTargetKind::Interface,
503 path.to_string(),
504 span,
505 doc_comment,
506 );
507 self.collect_type_params(path, interface_def.type_params.as_deref());
508 for member in &interface_def.members {
509 let (kind, name) = match member {
510 crate::ast::InterfaceMember::Property { name, .. } => {
511 (DocTargetKind::InterfaceProperty, name.as_str())
512 }
513 crate::ast::InterfaceMember::Method { name, .. } => {
514 (DocTargetKind::InterfaceMethod, name.as_str())
515 }
516 crate::ast::InterfaceMember::IndexSignature { param_type, .. } => {
517 (DocTargetKind::InterfaceIndexSignature, param_type.as_str())
518 }
519 };
520 let child_name = if matches!(kind, DocTargetKind::InterfaceIndexSignature) {
521 format!("[{}]", name)
522 } else {
523 name.to_string()
524 };
525 self.attach_comment(
526 kind,
527 join_child_path(path, &child_name),
528 member.span(),
529 member.doc_comment(),
530 );
531 }
532 }
533
534 fn collect_trait(
535 &mut self,
536 path: &str,
537 span: Span,
538 doc_comment: Option<&DocComment>,
539 trait_def: &crate::ast::TraitDef,
540 ) {
541 self.attach_comment(DocTargetKind::Trait, path.to_string(), span, doc_comment);
542 self.collect_type_params(path, trait_def.type_params.as_deref());
543 for member in &trait_def.members {
544 let (kind, child_name, child_span) = match member {
545 TraitMember::Required(crate::ast::InterfaceMember::Property {
546 name, span, ..
547 })
548 | TraitMember::Required(crate::ast::InterfaceMember::Method {
549 name, span, ..
550 }) => (DocTargetKind::TraitMethod, name.clone(), *span),
551 TraitMember::Required(crate::ast::InterfaceMember::IndexSignature {
552 param_type,
553 span,
554 ..
555 }) => (
556 DocTargetKind::TraitMethod,
557 format!("[{}]", param_type),
558 *span,
559 ),
560 TraitMember::Default(method) => {
561 (DocTargetKind::TraitMethod, method.name.clone(), method.span)
562 }
563 TraitMember::AssociatedType { name, span, .. } => {
564 (DocTargetKind::TraitAssociatedType, name.clone(), *span)
565 }
566 };
567 self.attach_comment(
568 kind,
569 join_child_path(path, &child_name),
570 child_span,
571 member.doc_comment(),
572 );
573 }
574 }
575
576 fn collect_extend(
577 &mut self,
578 module_path: &[String],
579 _span: Span,
580 extend: &crate::ast::ExtendStatement,
581 ) {
582 for method in &extend.methods {
583 self.attach_comment(
584 DocTargetKind::ExtensionMethod,
585 extend_method_doc_path(module_path, &extend.type_name, &method.name),
586 method.span,
587 method.doc_comment.as_ref(),
588 );
589 }
590 }
591
592 fn collect_impl(
593 &mut self,
594 module_path: &[String],
595 _span: Span,
596 impl_block: &crate::ast::ImplBlock,
597 ) {
598 for method in &impl_block.methods {
599 self.attach_comment(
600 DocTargetKind::ImplMethod,
601 impl_method_doc_path(
602 module_path,
603 &impl_block.trait_name,
604 &impl_block.target_type,
605 &method.name,
606 ),
607 method.span,
608 method.doc_comment.as_ref(),
609 );
610 }
611 }
612
613 fn collect_type_params(&mut self, parent_path: &str, type_params: Option<&[TypeParam]>) {
614 for type_param in type_params.unwrap_or(&[]) {
615 self.attach_comment(
616 DocTargetKind::TypeParam,
617 join_type_param_path(parent_path, &type_param.name),
618 type_param.span,
619 type_param.doc_comment.as_ref(),
620 );
621 }
622 }
623
624 fn attach_comment(
625 &mut self,
626 kind: DocTargetKind,
627 path: String,
628 span: Span,
629 doc_comment: Option<&DocComment>,
630 ) {
631 let Some(comment) = doc_comment.cloned() else {
632 return;
633 };
634 self.entries.push(DocEntry {
635 target: DocTarget { kind, path, span },
636 comment,
637 });
638 }
639}
640
641fn append_path(module_path: &[String], name: &str) -> Vec<String> {
642 let mut next = module_path.to_vec();
643 next.push(name.to_string());
644 next
645}
646
647fn join_path(prefix: &[String], name: &str) -> String {
648 if prefix.is_empty() {
649 name.to_string()
650 } else {
651 format!("{}::{}", prefix.join("::"), name)
652 }
653}
654
655fn join_annotation_path(prefix: &[String], name: &str) -> String {
656 join_path(prefix, &format!("@{name}"))
657}
658
659fn join_child_path(parent: &str, name: &str) -> String {
660 format!("{}::{}", parent, name)
661}
662
663fn join_type_param_path(parent: &str, name: &str) -> String {
664 format!("{}::<{}>", parent, name)
665}
666
667fn module_path_from_comment(comment: &DocComment) -> Option<String> {
668 comment.tags.iter().find_map(|tag| match tag.kind {
669 DocTagKind::Module if !tag.body.trim().is_empty() => Some(tag.body.trim().to_string()),
670 _ => None,
671 })
672}
673
674#[cfg(test)]
675mod tests {
676 use crate::ast::{DocTargetKind, Item};
677 use crate::parser::parse_program;
678
679 #[test]
680 fn attaches_docs_to_top_level_items() {
681 let program = parse_program("/// Adds\nfn add(x: number) -> number { x }\n")
682 .expect("program should parse");
683 let doc = program
684 .docs
685 .comment_for_path("add")
686 .expect("doc for function");
687 assert_eq!(doc.summary, "Adds");
688 }
689
690 #[test]
691 fn attaches_docs_to_program_modules() {
692 let source =
693 "/// @module std::core::json_value\n/// Typed JSON values.\npub enum Json { Null }\n";
694 let program = parse_program(source).expect("program should parse");
695 let entry = program
696 .docs
697 .entry_for_path("std::core::json_value")
698 .expect("doc entry for module");
699 assert_eq!(entry.target.kind, DocTargetKind::Module);
700 assert_eq!(entry.comment.summary, "Typed JSON values.");
701 }
702
703 #[test]
704 fn attaches_docs_to_struct_members() {
705 let source = "type Point {\n /// X coordinate\n x: number,\n}\n";
706 let program = parse_program(source).expect("program should parse");
707 let doc = program
708 .docs
709 .comment_for_path("Point::x")
710 .expect("doc for field");
711 assert_eq!(doc.summary, "X coordinate");
712 }
713
714 #[test]
715 fn attaches_docs_to_type_params() {
716 let source = "fn identity<\n /// Input type\n T\n>(value: T) -> T { value }\n";
717 let program = parse_program(source).expect("program should parse");
718 let entry = program
719 .docs
720 .entry_for_path("identity::<T>")
721 .expect("doc for type param");
722 assert_eq!(entry.target.kind, DocTargetKind::TypeParam);
723 assert_eq!(entry.comment.summary, "Input type");
724 }
725
726 #[test]
727 fn parses_structured_tags() {
728 let source = "/// Summary\n/// @param x value\nfn add(x: number) -> number { x }\n";
729 let program = parse_program(source).expect("program should parse");
730 let doc = program
731 .docs
732 .comment_for_path("add")
733 .expect("doc for function");
734 assert_eq!(doc.param_doc("x"), Some("value"));
735 }
736
737 #[test]
738 fn attaches_docs_to_annotation_defs() {
739 let source = "/// Configures warmup handling.\n/// @param period Number of lookback bars.\nannotation warmup(period) { metadata() { return { warmup: period } } }\n";
740 let program = parse_program(source).expect("program should parse");
741 let entry = program
742 .docs
743 .entry_for_path("@warmup")
744 .expect("doc for annotation");
745 assert_eq!(entry.target.kind, DocTargetKind::Annotation);
746 assert_eq!(
747 entry.comment.param_doc("period"),
748 Some("Number of lookback bars.")
749 );
750 }
751
752 #[test]
753 fn block_doc_comments_do_not_create_docs() {
754 let source = "/** Old style */\nfn add(x: number) -> number { x }\n";
755 let program = parse_program(source).expect("program should parse");
756 assert!(program.docs.comment_for_path("add").is_none());
757 }
758
759 #[test]
760 fn attaches_docs_to_extend_methods() {
761 let source = "extend Json {\n /// Access a field.\n method get(key: string) -> Json { self }\n}\n";
762 let program = parse_program(source).expect("program should parse");
763 let extend = program
764 .items
765 .iter()
766 .find_map(|item| match item {
767 Item::Extend(extend, _) => Some(extend),
768 _ => None,
769 })
770 .expect("extend block");
771 let method = extend.methods.first().expect("extend method");
772 let entry = program
773 .docs
774 .entry_for_span(method.span)
775 .expect("doc entry for extend method");
776 assert_eq!(entry.target.kind, DocTargetKind::ExtensionMethod);
777 assert_eq!(entry.comment.summary, "Access a field.");
778 }
779
780 #[test]
781 fn attaches_docs_to_impl_methods() {
782 let source = "impl Display for Json {\n /// Render the value.\n method render() -> string { \"json\" }\n}\n";
783 let program = parse_program(source).expect("program should parse");
784 let impl_block = program
785 .items
786 .iter()
787 .find_map(|item| match item {
788 Item::Impl(impl_block, _) => Some(impl_block),
789 _ => None,
790 })
791 .expect("impl block");
792 let method = impl_block.methods.first().expect("impl method");
793 let entry = program
794 .docs
795 .entry_for_span(method.span)
796 .expect("doc entry for impl method");
797 assert_eq!(entry.target.kind, DocTargetKind::ImplMethod);
798 assert_eq!(entry.comment.summary, "Render the value.");
799 }
800
801 #[test]
802 fn parses_stdlib_json_value_module_with_documented_methods() {
803 let source = include_str!("../../../shape-core/stdlib/core/json_value.shape");
804 let program = parse_program(source).expect("stdlib json_value module should parse");
805 assert!(
806 program
807 .docs
808 .entry_for_path("std::core::json_value")
809 .is_some()
810 );
811 assert!(
812 program
813 .docs
814 .entries
815 .iter()
816 .any(|entry| entry.target.kind == DocTargetKind::ExtensionMethod),
817 "expected documented extension methods in std::core::json_value"
818 );
819 }
820}