Skip to main content

shape_ast/parser/
docs.rs

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}