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