1use crate::module_cache::ModuleCache;
2use shape_ast::ast::{
3 DocTargetKind, ExportItem, FunctionParameter, InterfaceMember, Item, Program, Span,
4 TraitMember, TypeAnnotation, TypeParam, extend_method_doc_path, impl_method_doc_path,
5};
6use std::collections::BTreeSet;
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone)]
10pub struct DocSymbol {
11 pub kind: DocTargetKind,
12 pub local_path: String,
13 pub qualified_path: String,
14 pub span: Span,
15}
16
17#[derive(Debug, Clone, Default)]
18pub struct DocOwner {
19 pub params: Vec<String>,
20 pub type_params: Vec<String>,
21 pub can_have_return_doc: bool,
22}
23
24pub fn span_contains(span: Span, offset: usize) -> bool {
25 !span.is_dummy() && span.start <= offset && offset <= span.end
26}
27
28pub fn qualify_doc_path(module_prefix: &str, local_path: impl AsRef<str>) -> String {
29 let local_path = local_path.as_ref();
30 if module_prefix.is_empty() {
31 local_path.to_string()
32 } else if local_path.is_empty() {
33 module_prefix.to_string()
34 } else {
35 format!("{module_prefix}::{local_path}")
36 }
37}
38
39pub fn current_module_import_path(
40 cache: Option<&ModuleCache>,
41 current_file: Option<&Path>,
42 workspace_root: Option<&Path>,
43) -> Option<String> {
44 let (cache, file_path) = (cache?, current_file?);
45 let current = normalize_path(file_path);
46
47 for module_path in cache.list_importable_modules_with_context(file_path, workspace_root) {
48 let Some(resolved) = cache.resolve_import(&module_path, file_path, workspace_root) else {
49 continue;
50 };
51 if normalize_path(&resolved) == current {
52 return Some(module_path);
53 }
54 }
55
56 None
57}
58
59pub fn collect_import_paths(program: &Program) -> BTreeSet<String> {
60 let mut imports = BTreeSet::new();
61 for item in &program.items {
62 if let Item::Import(import_stmt, _) = item {
63 imports.insert(import_stmt.from.clone());
64 }
65 }
66 imports
67}
68
69pub fn collect_program_doc_symbols(program: &Program, module_prefix: &str) -> Vec<DocSymbol> {
70 let mut out = Vec::new();
71 collect_doc_symbols_in_items(&program.items, module_prefix, &[], &mut out);
72 out
73}
74
75pub fn find_doc_owner(program: &Program, target_span: Span) -> Option<DocOwner> {
76 program
77 .items
78 .iter()
79 .find_map(|item| find_doc_owner_in_item(item, target_span))
80}
81
82fn collect_doc_symbols_in_items(
83 items: &[Item],
84 module_prefix: &str,
85 path_prefix: &[String],
86 out: &mut Vec<DocSymbol>,
87) {
88 for item in items {
89 match item {
90 Item::Module(module, span) => {
91 let path = join_path(path_prefix, &module.name);
92 push_symbol(
93 out,
94 DocTargetKind::Module,
95 module_prefix,
96 path.clone(),
97 *span,
98 );
99 let mut next = path_prefix.to_vec();
100 next.push(module.name.clone());
101 collect_doc_symbols_in_items(&module.items, module_prefix, &next, out);
102 }
103 Item::Function(function, span) => {
104 push_symbol(
105 out,
106 DocTargetKind::Function,
107 module_prefix,
108 join_path(path_prefix, &function.name),
109 *span,
110 );
111 push_type_params(
112 out,
113 module_prefix,
114 path_prefix,
115 &function.name,
116 function.type_params.as_deref(),
117 );
118 }
119 Item::AnnotationDef(annotation_def, span) => {
120 push_symbol(
121 out,
122 DocTargetKind::Annotation,
123 module_prefix,
124 join_annotation_path(path_prefix, &annotation_def.name),
125 *span,
126 );
127 }
128 Item::ForeignFunction(function, span) => {
129 push_symbol(
130 out,
131 DocTargetKind::ForeignFunction,
132 module_prefix,
133 join_path(path_prefix, &function.name),
134 *span,
135 );
136 push_type_params(
137 out,
138 module_prefix,
139 path_prefix,
140 &function.name,
141 function.type_params.as_deref(),
142 );
143 }
144 Item::BuiltinFunctionDecl(function, span) => {
145 push_symbol(
146 out,
147 DocTargetKind::BuiltinFunction,
148 module_prefix,
149 join_path(path_prefix, &function.name),
150 *span,
151 );
152 push_type_params(
153 out,
154 module_prefix,
155 path_prefix,
156 &function.name,
157 function.type_params.as_deref(),
158 );
159 }
160 Item::BuiltinTypeDecl(ty, span) => {
161 push_symbol(
162 out,
163 DocTargetKind::BuiltinType,
164 module_prefix,
165 join_path(path_prefix, &ty.name),
166 *span,
167 );
168 push_type_params(
169 out,
170 module_prefix,
171 path_prefix,
172 &ty.name,
173 ty.type_params.as_deref(),
174 );
175 }
176 Item::TypeAlias(alias, span) => {
177 push_symbol(
178 out,
179 DocTargetKind::TypeAlias,
180 module_prefix,
181 join_path(path_prefix, &alias.name),
182 *span,
183 );
184 push_type_params(
185 out,
186 module_prefix,
187 path_prefix,
188 &alias.name,
189 alias.type_params.as_deref(),
190 );
191 }
192 Item::StructType(struct_def, span) => {
193 let path = join_path(path_prefix, &struct_def.name);
194 push_symbol(
195 out,
196 DocTargetKind::Struct,
197 module_prefix,
198 path.clone(),
199 *span,
200 );
201 push_type_params(
202 out,
203 module_prefix,
204 path_prefix,
205 &struct_def.name,
206 struct_def.type_params.as_deref(),
207 );
208 for field in &struct_def.fields {
209 push_symbol(
210 out,
211 DocTargetKind::StructField,
212 module_prefix,
213 join_child_path(&path, &field.name),
214 field.span,
215 );
216 }
217 }
218 Item::Enum(enum_def, span) => {
219 let path = join_path(path_prefix, &enum_def.name);
220 push_symbol(out, DocTargetKind::Enum, module_prefix, path.clone(), *span);
221 push_type_params(
222 out,
223 module_prefix,
224 path_prefix,
225 &enum_def.name,
226 enum_def.type_params.as_deref(),
227 );
228 for member in &enum_def.members {
229 push_symbol(
230 out,
231 DocTargetKind::EnumVariant,
232 module_prefix,
233 join_child_path(&path, &member.name),
234 member.span,
235 );
236 }
237 }
238 Item::Interface(interface, span) => {
239 let path = join_path(path_prefix, &interface.name);
240 push_symbol(
241 out,
242 DocTargetKind::Interface,
243 module_prefix,
244 path.clone(),
245 *span,
246 );
247 push_type_params(
248 out,
249 module_prefix,
250 path_prefix,
251 &interface.name,
252 interface.type_params.as_deref(),
253 );
254 for member in &interface.members {
255 push_symbol(
256 out,
257 interface_member_kind(member),
258 module_prefix,
259 join_child_path(&path, &interface_member_name(member)),
260 member.span(),
261 );
262 }
263 }
264 Item::Trait(trait_def, span) => {
265 let path = join_path(path_prefix, &trait_def.name);
266 push_symbol(
267 out,
268 DocTargetKind::Trait,
269 module_prefix,
270 path.clone(),
271 *span,
272 );
273 push_type_params(
274 out,
275 module_prefix,
276 path_prefix,
277 &trait_def.name,
278 trait_def.type_params.as_deref(),
279 );
280 for member in &trait_def.members {
281 push_symbol(
282 out,
283 trait_member_kind(member),
284 module_prefix,
285 join_child_path(&path, &trait_member_name(member)),
286 member.span(),
287 );
288 }
289 }
290 Item::Extend(extend, _) => {
291 for method in &extend.methods {
292 push_symbol(
293 out,
294 DocTargetKind::ExtensionMethod,
295 module_prefix,
296 extend_method_doc_path(path_prefix, &extend.type_name, &method.name),
297 method.span,
298 );
299 }
300 }
301 Item::Impl(impl_block, _) => {
302 for method in &impl_block.methods {
303 push_symbol(
304 out,
305 DocTargetKind::ImplMethod,
306 module_prefix,
307 impl_method_doc_path(
308 path_prefix,
309 &impl_block.trait_name,
310 &impl_block.target_type,
311 &method.name,
312 ),
313 method.span,
314 );
315 }
316 }
317 Item::Export(export, span) => {
318 collect_export_symbols(out, module_prefix, path_prefix, export, *span);
319 }
320 _ => {}
321 }
322 }
323}
324
325fn collect_export_symbols(
326 out: &mut Vec<DocSymbol>,
327 module_prefix: &str,
328 path_prefix: &[String],
329 export: &shape_ast::ast::ExportStmt,
330 span: Span,
331) {
332 match &export.item {
333 ExportItem::Function(function) => {
334 push_symbol(
335 out,
336 DocTargetKind::Function,
337 module_prefix,
338 join_path(path_prefix, &function.name),
339 span,
340 );
341 push_type_params(
342 out,
343 module_prefix,
344 path_prefix,
345 &function.name,
346 function.type_params.as_deref(),
347 );
348 }
349 ExportItem::BuiltinFunction(function) => {
350 push_symbol(
351 out,
352 DocTargetKind::BuiltinFunction,
353 module_prefix,
354 join_path(path_prefix, &function.name),
355 span,
356 );
357 push_type_params(
358 out,
359 module_prefix,
360 path_prefix,
361 &function.name,
362 function.type_params.as_deref(),
363 );
364 }
365 ExportItem::ForeignFunction(function) => {
366 push_symbol(
367 out,
368 DocTargetKind::ForeignFunction,
369 module_prefix,
370 join_path(path_prefix, &function.name),
371 span,
372 );
373 push_type_params(
374 out,
375 module_prefix,
376 path_prefix,
377 &function.name,
378 function.type_params.as_deref(),
379 );
380 }
381 ExportItem::TypeAlias(alias) => {
382 push_symbol(
383 out,
384 DocTargetKind::TypeAlias,
385 module_prefix,
386 join_path(path_prefix, &alias.name),
387 span,
388 );
389 push_type_params(
390 out,
391 module_prefix,
392 path_prefix,
393 &alias.name,
394 alias.type_params.as_deref(),
395 );
396 }
397 ExportItem::BuiltinType(ty) => {
398 push_symbol(
399 out,
400 DocTargetKind::BuiltinType,
401 module_prefix,
402 join_path(path_prefix, &ty.name),
403 span,
404 );
405 push_type_params(
406 out,
407 module_prefix,
408 path_prefix,
409 &ty.name,
410 ty.type_params.as_deref(),
411 );
412 }
413 ExportItem::Struct(struct_def) => {
414 let path = join_path(path_prefix, &struct_def.name);
415 push_symbol(
416 out,
417 DocTargetKind::Struct,
418 module_prefix,
419 path.clone(),
420 span,
421 );
422 push_type_params(
423 out,
424 module_prefix,
425 path_prefix,
426 &struct_def.name,
427 struct_def.type_params.as_deref(),
428 );
429 for field in &struct_def.fields {
430 push_symbol(
431 out,
432 DocTargetKind::StructField,
433 module_prefix,
434 join_child_path(&path, &field.name),
435 field.span,
436 );
437 }
438 }
439 ExportItem::Enum(enum_def) => {
440 let path = join_path(path_prefix, &enum_def.name);
441 push_symbol(out, DocTargetKind::Enum, module_prefix, path.clone(), span);
442 push_type_params(
443 out,
444 module_prefix,
445 path_prefix,
446 &enum_def.name,
447 enum_def.type_params.as_deref(),
448 );
449 for member in &enum_def.members {
450 push_symbol(
451 out,
452 DocTargetKind::EnumVariant,
453 module_prefix,
454 join_child_path(&path, &member.name),
455 member.span,
456 );
457 }
458 }
459 ExportItem::Interface(interface) => {
460 let path = join_path(path_prefix, &interface.name);
461 push_symbol(
462 out,
463 DocTargetKind::Interface,
464 module_prefix,
465 path.clone(),
466 span,
467 );
468 push_type_params(
469 out,
470 module_prefix,
471 path_prefix,
472 &interface.name,
473 interface.type_params.as_deref(),
474 );
475 for member in &interface.members {
476 push_symbol(
477 out,
478 interface_member_kind(member),
479 module_prefix,
480 join_child_path(&path, &interface_member_name(member)),
481 member.span(),
482 );
483 }
484 }
485 ExportItem::Trait(trait_def) => {
486 let path = join_path(path_prefix, &trait_def.name);
487 push_symbol(out, DocTargetKind::Trait, module_prefix, path.clone(), span);
488 push_type_params(
489 out,
490 module_prefix,
491 path_prefix,
492 &trait_def.name,
493 trait_def.type_params.as_deref(),
494 );
495 for member in &trait_def.members {
496 push_symbol(
497 out,
498 trait_member_kind(member),
499 module_prefix,
500 join_child_path(&path, &trait_member_name(member)),
501 member.span(),
502 );
503 }
504 }
505 ExportItem::Annotation(annotation_def) => {
506 push_symbol(
507 out,
508 DocTargetKind::Annotation,
509 module_prefix,
510 join_path(path_prefix, &annotation_def.name),
511 span,
512 );
513 }
514 ExportItem::Named(_) => {}
515 }
516}
517
518fn push_type_params(
519 out: &mut Vec<DocSymbol>,
520 module_prefix: &str,
521 path_prefix: &[String],
522 owner_name: &str,
523 type_params: Option<&[TypeParam]>,
524) {
525 let owner_path = join_path(path_prefix, owner_name);
526 for type_param in type_params.unwrap_or(&[]) {
527 push_symbol(
528 out,
529 DocTargetKind::TypeParam,
530 module_prefix,
531 join_type_param_path(&owner_path, &type_param.name),
532 type_param.span,
533 );
534 }
535}
536
537fn push_symbol(
538 out: &mut Vec<DocSymbol>,
539 kind: DocTargetKind,
540 module_prefix: &str,
541 local_path: String,
542 span: Span,
543) {
544 out.push(DocSymbol {
545 kind,
546 qualified_path: qualify_doc_path(module_prefix, &local_path),
547 local_path,
548 span,
549 });
550}
551
552fn interface_member_kind(member: &InterfaceMember) -> DocTargetKind {
553 match member {
554 InterfaceMember::Property { .. } => DocTargetKind::InterfaceProperty,
555 InterfaceMember::Method { .. } => DocTargetKind::InterfaceMethod,
556 InterfaceMember::IndexSignature { .. } => DocTargetKind::InterfaceIndexSignature,
557 }
558}
559
560fn interface_member_name(member: &InterfaceMember) -> String {
561 match member {
562 InterfaceMember::Property { name, .. } | InterfaceMember::Method { name, .. } => {
563 name.clone()
564 }
565 InterfaceMember::IndexSignature { param_type, .. } => format!("[{param_type}]"),
566 }
567}
568
569fn trait_member_kind(member: &TraitMember) -> DocTargetKind {
570 match member {
571 TraitMember::AssociatedType { .. } => DocTargetKind::TraitAssociatedType,
572 TraitMember::Required(_) | TraitMember::Default(_) => DocTargetKind::TraitMethod,
573 }
574}
575
576fn trait_member_name(member: &TraitMember) -> String {
577 match member {
578 TraitMember::Required(member) => interface_member_name(member),
579 TraitMember::Default(method) => method.name.clone(),
580 TraitMember::AssociatedType { name, .. } => name.clone(),
581 }
582}
583
584fn find_doc_owner_in_item(item: &Item, target_span: Span) -> Option<DocOwner> {
585 match item {
586 Item::Module(module, span) => {
587 if *span == target_span {
588 return Some(DocOwner {
589 ..Default::default()
590 });
591 }
592 module
593 .items
594 .iter()
595 .find_map(|child| find_doc_owner_in_item(child, target_span))
596 }
597 Item::Function(function, span) if *span == target_span => Some(callable_owner(
598 DocTargetKind::Function,
599 &function.params,
600 function.type_params.as_deref(),
601 function.return_type.as_ref(),
602 )),
603 Item::AnnotationDef(annotation_def, span) if *span == target_span => Some(DocOwner {
604 params: function_param_names(&annotation_def.params),
605 can_have_return_doc: false,
606 ..Default::default()
607 }),
608 Item::ForeignFunction(function, span) if *span == target_span => Some(callable_owner(
609 DocTargetKind::ForeignFunction,
610 &function.params,
611 function.type_params.as_deref(),
612 function.return_type.as_ref(),
613 )),
614 Item::BuiltinFunctionDecl(function, span) if *span == target_span => Some(callable_owner(
615 DocTargetKind::BuiltinFunction,
616 &function.params,
617 function.type_params.as_deref(),
618 Some(&function.return_type),
619 )),
620 Item::BuiltinTypeDecl(ty, span) if *span == target_span => Some(type_owner(
621 DocTargetKind::BuiltinType,
622 ty.type_params.as_deref(),
623 )),
624 Item::TypeAlias(alias, span) if *span == target_span => Some(type_owner(
625 DocTargetKind::TypeAlias,
626 alias.type_params.as_deref(),
627 )),
628 Item::StructType(struct_def, span) if *span == target_span => Some(type_owner(
629 DocTargetKind::Struct,
630 struct_def.type_params.as_deref(),
631 )),
632 Item::Enum(enum_def, span) if *span == target_span => Some(type_owner(
633 DocTargetKind::Enum,
634 enum_def.type_params.as_deref(),
635 )),
636 Item::Interface(interface, span) if *span == target_span => Some(type_owner(
637 DocTargetKind::Interface,
638 interface.type_params.as_deref(),
639 )),
640 Item::Trait(trait_def, span) if *span == target_span => Some(type_owner(
641 DocTargetKind::Trait,
642 trait_def.type_params.as_deref(),
643 )),
644 Item::Interface(interface, _) => find_doc_owner_in_interface(interface, target_span),
645 Item::Trait(trait_def, _) => find_doc_owner_in_trait(trait_def, target_span),
646 Item::Extend(extend, _) => find_doc_owner_in_extend(extend, target_span),
647 Item::Impl(impl_block, _) => find_doc_owner_in_impl(impl_block, target_span),
648 Item::Export(export, span) if *span == target_span => Some(export_owner(export)),
649 _ => None,
650 }
651}
652
653fn find_doc_owner_in_interface(
654 interface: &shape_ast::ast::InterfaceDef,
655 target_span: Span,
656) -> Option<DocOwner> {
657 for member in &interface.members {
658 if member.span() != target_span {
659 continue;
660 }
661 return Some(match member {
662 InterfaceMember::Method {
663 params,
664 return_type,
665 ..
666 } => DocOwner {
667 params: interface_method_param_names(params),
668 can_have_return_doc: !matches!(return_type, TypeAnnotation::Void),
669 ..Default::default()
670 },
671 InterfaceMember::Property { .. } => DocOwner {
672 ..Default::default()
673 },
674 InterfaceMember::IndexSignature { .. } => DocOwner {
675 ..Default::default()
676 },
677 });
678 }
679 None
680}
681
682fn find_doc_owner_in_trait(
683 trait_def: &shape_ast::ast::TraitDef,
684 target_span: Span,
685) -> Option<DocOwner> {
686 for member in &trait_def.members {
687 if member.span() != target_span {
688 continue;
689 }
690 return Some(match member {
691 TraitMember::Default(method) => callable_owner(
692 DocTargetKind::TraitMethod,
693 &method.params,
694 None,
695 method.return_type.as_ref(),
696 ),
697 TraitMember::Required(InterfaceMember::Method {
698 params,
699 return_type,
700 ..
701 }) => DocOwner {
702 params: interface_method_param_names(params),
703 can_have_return_doc: !matches!(return_type, TypeAnnotation::Void),
704 ..Default::default()
705 },
706 TraitMember::Required(InterfaceMember::Property { .. })
707 | TraitMember::Required(InterfaceMember::IndexSignature { .. }) => DocOwner {
708 ..Default::default()
709 },
710 TraitMember::AssociatedType { .. } => DocOwner {
711 ..Default::default()
712 },
713 });
714 }
715 None
716}
717
718fn find_doc_owner_in_extend(
719 extend: &shape_ast::ast::ExtendStatement,
720 target_span: Span,
721) -> Option<DocOwner> {
722 extend.methods.iter().find_map(|method| {
723 (method.span == target_span).then(|| {
724 callable_owner(
725 DocTargetKind::ExtensionMethod,
726 &method.params,
727 None,
728 method.return_type.as_ref(),
729 )
730 })
731 })
732}
733
734fn find_doc_owner_in_impl(
735 impl_block: &shape_ast::ast::ImplBlock,
736 target_span: Span,
737) -> Option<DocOwner> {
738 impl_block.methods.iter().find_map(|method| {
739 (method.span == target_span).then(|| {
740 callable_owner(
741 DocTargetKind::ImplMethod,
742 &method.params,
743 None,
744 method.return_type.as_ref(),
745 )
746 })
747 })
748}
749
750fn export_owner(export: &shape_ast::ast::ExportStmt) -> DocOwner {
751 match &export.item {
752 ExportItem::Function(function) => callable_owner(
753 DocTargetKind::Function,
754 &function.params,
755 function.type_params.as_deref(),
756 function.return_type.as_ref(),
757 ),
758 ExportItem::BuiltinFunction(function) => callable_owner(
759 DocTargetKind::BuiltinFunction,
760 &function.params,
761 function.type_params.as_deref(),
762 Some(&function.return_type),
763 ),
764 ExportItem::ForeignFunction(function) => callable_owner(
765 DocTargetKind::ForeignFunction,
766 &function.params,
767 function.type_params.as_deref(),
768 function.return_type.as_ref(),
769 ),
770 ExportItem::TypeAlias(alias) => {
771 type_owner(DocTargetKind::TypeAlias, alias.type_params.as_deref())
772 }
773 ExportItem::BuiltinType(ty) => {
774 type_owner(DocTargetKind::BuiltinType, ty.type_params.as_deref())
775 }
776 ExportItem::Struct(struct_def) => {
777 type_owner(DocTargetKind::Struct, struct_def.type_params.as_deref())
778 }
779 ExportItem::Enum(enum_def) => {
780 type_owner(DocTargetKind::Enum, enum_def.type_params.as_deref())
781 }
782 ExportItem::Interface(interface) => {
783 type_owner(DocTargetKind::Interface, interface.type_params.as_deref())
784 }
785 ExportItem::Trait(trait_def) => {
786 type_owner(DocTargetKind::Trait, trait_def.type_params.as_deref())
787 }
788 ExportItem::Annotation(annotation_def) => callable_owner(
789 DocTargetKind::Annotation,
790 &annotation_def.params,
791 None,
792 None,
793 ),
794 ExportItem::Named(_) => DocOwner::default(),
795 }
796}
797
798fn callable_owner(
799 _kind: DocTargetKind,
800 params: &[FunctionParameter],
801 type_params: Option<&[TypeParam]>,
802 return_type: Option<&TypeAnnotation>,
803) -> DocOwner {
804 DocOwner {
805 params: function_param_names(params),
806 type_params: type_param_names(type_params),
807 can_have_return_doc: !matches!(return_type, Some(TypeAnnotation::Void)),
808 }
809}
810
811fn type_owner(_kind: DocTargetKind, type_params: Option<&[TypeParam]>) -> DocOwner {
812 DocOwner {
813 type_params: type_param_names(type_params),
814 ..Default::default()
815 }
816}
817
818pub fn type_param_names(type_params: Option<&[TypeParam]>) -> Vec<String> {
819 type_params
820 .unwrap_or(&[])
821 .iter()
822 .map(|param| param.name.clone())
823 .collect()
824}
825
826pub fn function_param_names(params: &[FunctionParameter]) -> Vec<String> {
827 let mut names = Vec::new();
828 for param in params {
829 names.extend(param.get_identifiers());
830 }
831 names.sort();
832 names.dedup();
833 names
834}
835
836fn interface_method_param_names(params: &[shape_ast::ast::FunctionParam]) -> Vec<String> {
837 let mut names = params
838 .iter()
839 .filter_map(|param| param.name.clone())
840 .collect::<Vec<_>>();
841 names.sort();
842 names.dedup();
843 names
844}
845
846fn join_path(prefix: &[String], name: &str) -> String {
847 if prefix.is_empty() {
848 name.to_string()
849 } else {
850 format!("{}::{}", prefix.join("::"), name)
851 }
852}
853
854fn join_annotation_path(prefix: &[String], name: &str) -> String {
855 join_path(prefix, &format!("@{name}"))
856}
857
858fn join_child_path(parent: &str, name: &str) -> String {
859 format!("{parent}::{name}")
860}
861
862fn join_type_param_path(parent: &str, name: &str) -> String {
863 format!("{parent}::<{name}>")
864}
865
866fn normalize_path(path: &Path) -> PathBuf {
867 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873 use shape_ast::parser::parse_program;
874
875 #[test]
876 fn collects_member_symbols_with_qualified_paths() {
877 let program = parse_program(
878 "type Point { /// x\n x: number }\ntrait Drawable { /// draw\n draw(): void }\n",
879 )
880 .expect("program");
881 let symbols = collect_program_doc_symbols(&program, "pkg::math");
882 assert!(
883 symbols
884 .iter()
885 .any(|symbol| symbol.qualified_path == "pkg::math::Point::x")
886 );
887 assert!(
888 symbols
889 .iter()
890 .any(|symbol| symbol.qualified_path == "pkg::math::Drawable::draw")
891 );
892 }
893
894 #[test]
895 fn collects_annotation_symbols_with_canonical_paths() {
896 let program =
897 parse_program("/// Trace execution.\nannotation trace() {}\n").expect("program");
898 let symbols = collect_program_doc_symbols(&program, "pkg::debug");
899 assert!(
900 symbols
901 .iter()
902 .any(|symbol| symbol.qualified_path == "pkg::debug::@trace")
903 );
904 }
905}