1use crate::module_cache::ModuleCache;
2use shape_ast::ast::{
3 DocTargetKind, ExportItem, FunctionParameter, TraitMemberSignature, 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::Trait(trait_def, span) => {
239 let path = join_path(path_prefix, &trait_def.name);
240 push_symbol(
241 out,
242 DocTargetKind::Trait,
243 module_prefix,
244 path.clone(),
245 *span,
246 );
247 push_type_params(
248 out,
249 module_prefix,
250 path_prefix,
251 &trait_def.name,
252 trait_def.type_params.as_deref(),
253 );
254 for member in &trait_def.members {
255 push_symbol(
256 out,
257 trait_member_kind(member),
258 module_prefix,
259 join_child_path(&path, &trait_member_name(member)),
260 member.span(),
261 );
262 }
263 }
264 Item::Extend(extend, _) => {
265 for method in &extend.methods {
266 push_symbol(
267 out,
268 DocTargetKind::ExtensionMethod,
269 module_prefix,
270 extend_method_doc_path(path_prefix, &extend.type_name, &method.name),
271 method.span,
272 );
273 }
274 }
275 Item::Impl(impl_block, _) => {
276 for method in &impl_block.methods {
277 push_symbol(
278 out,
279 DocTargetKind::ImplMethod,
280 module_prefix,
281 impl_method_doc_path(
282 path_prefix,
283 &impl_block.trait_name,
284 &impl_block.target_type,
285 &method.name,
286 ),
287 method.span,
288 );
289 }
290 }
291 Item::Export(export, span) => {
292 collect_export_symbols(out, module_prefix, path_prefix, export, *span);
293 }
294 _ => {}
295 }
296 }
297}
298
299fn collect_export_symbols(
300 out: &mut Vec<DocSymbol>,
301 module_prefix: &str,
302 path_prefix: &[String],
303 export: &shape_ast::ast::ExportStmt,
304 span: Span,
305) {
306 match &export.item {
307 ExportItem::Function(function) => {
308 push_symbol(
309 out,
310 DocTargetKind::Function,
311 module_prefix,
312 join_path(path_prefix, &function.name),
313 span,
314 );
315 push_type_params(
316 out,
317 module_prefix,
318 path_prefix,
319 &function.name,
320 function.type_params.as_deref(),
321 );
322 }
323 ExportItem::BuiltinFunction(function) => {
324 push_symbol(
325 out,
326 DocTargetKind::BuiltinFunction,
327 module_prefix,
328 join_path(path_prefix, &function.name),
329 span,
330 );
331 push_type_params(
332 out,
333 module_prefix,
334 path_prefix,
335 &function.name,
336 function.type_params.as_deref(),
337 );
338 }
339 ExportItem::ForeignFunction(function) => {
340 push_symbol(
341 out,
342 DocTargetKind::ForeignFunction,
343 module_prefix,
344 join_path(path_prefix, &function.name),
345 span,
346 );
347 push_type_params(
348 out,
349 module_prefix,
350 path_prefix,
351 &function.name,
352 function.type_params.as_deref(),
353 );
354 }
355 ExportItem::TypeAlias(alias) => {
356 push_symbol(
357 out,
358 DocTargetKind::TypeAlias,
359 module_prefix,
360 join_path(path_prefix, &alias.name),
361 span,
362 );
363 push_type_params(
364 out,
365 module_prefix,
366 path_prefix,
367 &alias.name,
368 alias.type_params.as_deref(),
369 );
370 }
371 ExportItem::BuiltinType(ty) => {
372 push_symbol(
373 out,
374 DocTargetKind::BuiltinType,
375 module_prefix,
376 join_path(path_prefix, &ty.name),
377 span,
378 );
379 push_type_params(
380 out,
381 module_prefix,
382 path_prefix,
383 &ty.name,
384 ty.type_params.as_deref(),
385 );
386 }
387 ExportItem::Struct(struct_def) => {
388 let path = join_path(path_prefix, &struct_def.name);
389 push_symbol(
390 out,
391 DocTargetKind::Struct,
392 module_prefix,
393 path.clone(),
394 span,
395 );
396 push_type_params(
397 out,
398 module_prefix,
399 path_prefix,
400 &struct_def.name,
401 struct_def.type_params.as_deref(),
402 );
403 for field in &struct_def.fields {
404 push_symbol(
405 out,
406 DocTargetKind::StructField,
407 module_prefix,
408 join_child_path(&path, &field.name),
409 field.span,
410 );
411 }
412 }
413 ExportItem::Enum(enum_def) => {
414 let path = join_path(path_prefix, &enum_def.name);
415 push_symbol(out, DocTargetKind::Enum, module_prefix, path.clone(), span);
416 push_type_params(
417 out,
418 module_prefix,
419 path_prefix,
420 &enum_def.name,
421 enum_def.type_params.as_deref(),
422 );
423 for member in &enum_def.members {
424 push_symbol(
425 out,
426 DocTargetKind::EnumVariant,
427 module_prefix,
428 join_child_path(&path, &member.name),
429 member.span,
430 );
431 }
432 }
433 ExportItem::Trait(trait_def) => {
434 let path = join_path(path_prefix, &trait_def.name);
435 push_symbol(out, DocTargetKind::Trait, module_prefix, path.clone(), span);
436 push_type_params(
437 out,
438 module_prefix,
439 path_prefix,
440 &trait_def.name,
441 trait_def.type_params.as_deref(),
442 );
443 for member in &trait_def.members {
444 push_symbol(
445 out,
446 trait_member_kind(member),
447 module_prefix,
448 join_child_path(&path, &trait_member_name(member)),
449 member.span(),
450 );
451 }
452 }
453 ExportItem::Annotation(annotation_def) => {
454 push_symbol(
455 out,
456 DocTargetKind::Annotation,
457 module_prefix,
458 join_path(path_prefix, &annotation_def.name),
459 span,
460 );
461 }
462 ExportItem::Named(_) => {}
463 }
464}
465
466fn push_type_params(
467 out: &mut Vec<DocSymbol>,
468 module_prefix: &str,
469 path_prefix: &[String],
470 owner_name: &str,
471 type_params: Option<&[TypeParam]>,
472) {
473 let owner_path = join_path(path_prefix, owner_name);
474 for type_param in type_params.unwrap_or(&[]) {
475 push_symbol(
476 out,
477 DocTargetKind::TypeParam,
478 module_prefix,
479 join_type_param_path(&owner_path, type_param.name()),
480 *type_param.span(),
481 );
482 }
483}
484
485fn push_symbol(
486 out: &mut Vec<DocSymbol>,
487 kind: DocTargetKind,
488 module_prefix: &str,
489 local_path: String,
490 span: Span,
491) {
492 out.push(DocSymbol {
493 kind,
494 qualified_path: qualify_doc_path(module_prefix, &local_path),
495 local_path,
496 span,
497 });
498}
499
500fn trait_member_signature_kind(member: &TraitMemberSignature) -> DocTargetKind {
501 match member {
502 TraitMemberSignature::Property { .. } => DocTargetKind::TraitProperty,
503 TraitMemberSignature::Method { .. } => DocTargetKind::TraitMethod,
504 TraitMemberSignature::IndexSignature { .. } => DocTargetKind::TraitIndexSignature,
505 }
506}
507
508fn trait_member_signature_name(member: &TraitMemberSignature) -> String {
509 match member {
510 TraitMemberSignature::Property { name, .. } | TraitMemberSignature::Method { name, .. } => {
511 name.clone()
512 }
513 TraitMemberSignature::IndexSignature { param_type, .. } => format!("[{param_type}]"),
514 }
515}
516
517fn trait_member_kind(member: &TraitMember) -> DocTargetKind {
518 match member {
519 TraitMember::AssociatedType { .. } => DocTargetKind::TraitAssociatedType,
520 TraitMember::Required(sig) => trait_member_signature_kind(sig),
521 TraitMember::Default(_) => DocTargetKind::TraitMethod,
522 }
523}
524
525fn trait_member_name(member: &TraitMember) -> String {
526 match member {
527 TraitMember::Required(member) => trait_member_signature_name(member),
528 TraitMember::Default(method) => method.name.clone(),
529 TraitMember::AssociatedType { name, .. } => name.clone(),
530 }
531}
532
533fn find_doc_owner_in_item(item: &Item, target_span: Span) -> Option<DocOwner> {
534 match item {
535 Item::Module(module, span) => {
536 if *span == target_span {
537 return Some(DocOwner {
538 ..Default::default()
539 });
540 }
541 module
542 .items
543 .iter()
544 .find_map(|child| find_doc_owner_in_item(child, target_span))
545 }
546 Item::Function(function, span) if *span == target_span => Some(callable_owner(
547 DocTargetKind::Function,
548 &function.params,
549 function.type_params.as_deref(),
550 function.return_type.as_ref(),
551 )),
552 Item::AnnotationDef(annotation_def, span) if *span == target_span => Some(DocOwner {
553 params: function_param_names(&annotation_def.params),
554 can_have_return_doc: false,
555 ..Default::default()
556 }),
557 Item::ForeignFunction(function, span) if *span == target_span => Some(callable_owner(
558 DocTargetKind::ForeignFunction,
559 &function.params,
560 function.type_params.as_deref(),
561 function.return_type.as_ref(),
562 )),
563 Item::BuiltinFunctionDecl(function, span) if *span == target_span => Some(callable_owner(
564 DocTargetKind::BuiltinFunction,
565 &function.params,
566 function.type_params.as_deref(),
567 Some(&function.return_type),
568 )),
569 Item::BuiltinTypeDecl(ty, span) if *span == target_span => Some(type_owner(
570 DocTargetKind::BuiltinType,
571 ty.type_params.as_deref(),
572 )),
573 Item::TypeAlias(alias, span) if *span == target_span => Some(type_owner(
574 DocTargetKind::TypeAlias,
575 alias.type_params.as_deref(),
576 )),
577 Item::StructType(struct_def, span) if *span == target_span => Some(type_owner(
578 DocTargetKind::Struct,
579 struct_def.type_params.as_deref(),
580 )),
581 Item::Enum(enum_def, span) if *span == target_span => Some(type_owner(
582 DocTargetKind::Enum,
583 enum_def.type_params.as_deref(),
584 )),
585 Item::Trait(trait_def, span) if *span == target_span => Some(type_owner(
586 DocTargetKind::Trait,
587 trait_def.type_params.as_deref(),
588 )),
589 Item::Trait(trait_def, _) => find_doc_owner_in_trait(trait_def, target_span),
590 Item::Extend(extend, _) => find_doc_owner_in_extend(extend, target_span),
591 Item::Impl(impl_block, _) => find_doc_owner_in_impl(impl_block, target_span),
592 Item::Export(export, span) if *span == target_span => Some(export_owner(export)),
593 _ => None,
594 }
595}
596
597fn find_doc_owner_in_trait(
598 trait_def: &shape_ast::ast::TraitDef,
599 target_span: Span,
600) -> Option<DocOwner> {
601 for member in &trait_def.members {
602 if member.span() != target_span {
603 continue;
604 }
605 return Some(match member {
606 TraitMember::Default(method) => callable_owner(
607 DocTargetKind::TraitMethod,
608 &method.params,
609 None,
610 method.return_type.as_ref(),
611 ),
612 TraitMember::Required(TraitMemberSignature::Method {
613 params,
614 return_type,
615 ..
616 }) => DocOwner {
617 params: trait_method_param_names(params),
618 can_have_return_doc: !matches!(return_type, TypeAnnotation::Void),
619 ..Default::default()
620 },
621 TraitMember::Required(TraitMemberSignature::Property { .. })
622 | TraitMember::Required(TraitMemberSignature::IndexSignature { .. }) => DocOwner {
623 ..Default::default()
624 },
625 TraitMember::AssociatedType { .. } => DocOwner {
626 ..Default::default()
627 },
628 });
629 }
630 None
631}
632
633fn find_doc_owner_in_extend(
634 extend: &shape_ast::ast::ExtendStatement,
635 target_span: Span,
636) -> Option<DocOwner> {
637 extend.methods.iter().find_map(|method| {
638 (method.span == target_span).then(|| {
639 callable_owner(
640 DocTargetKind::ExtensionMethod,
641 &method.params,
642 None,
643 method.return_type.as_ref(),
644 )
645 })
646 })
647}
648
649fn find_doc_owner_in_impl(
650 impl_block: &shape_ast::ast::ImplBlock,
651 target_span: Span,
652) -> Option<DocOwner> {
653 impl_block.methods.iter().find_map(|method| {
654 (method.span == target_span).then(|| {
655 callable_owner(
656 DocTargetKind::ImplMethod,
657 &method.params,
658 None,
659 method.return_type.as_ref(),
660 )
661 })
662 })
663}
664
665fn export_owner(export: &shape_ast::ast::ExportStmt) -> DocOwner {
666 match &export.item {
667 ExportItem::Function(function) => callable_owner(
668 DocTargetKind::Function,
669 &function.params,
670 function.type_params.as_deref(),
671 function.return_type.as_ref(),
672 ),
673 ExportItem::BuiltinFunction(function) => callable_owner(
674 DocTargetKind::BuiltinFunction,
675 &function.params,
676 function.type_params.as_deref(),
677 Some(&function.return_type),
678 ),
679 ExportItem::ForeignFunction(function) => callable_owner(
680 DocTargetKind::ForeignFunction,
681 &function.params,
682 function.type_params.as_deref(),
683 function.return_type.as_ref(),
684 ),
685 ExportItem::TypeAlias(alias) => {
686 type_owner(DocTargetKind::TypeAlias, alias.type_params.as_deref())
687 }
688 ExportItem::BuiltinType(ty) => {
689 type_owner(DocTargetKind::BuiltinType, ty.type_params.as_deref())
690 }
691 ExportItem::Struct(struct_def) => {
692 type_owner(DocTargetKind::Struct, struct_def.type_params.as_deref())
693 }
694 ExportItem::Enum(enum_def) => {
695 type_owner(DocTargetKind::Enum, enum_def.type_params.as_deref())
696 }
697 ExportItem::Trait(trait_def) => {
698 type_owner(DocTargetKind::Trait, trait_def.type_params.as_deref())
699 }
700 ExportItem::Annotation(annotation_def) => callable_owner(
701 DocTargetKind::Annotation,
702 &annotation_def.params,
703 None,
704 None,
705 ),
706 ExportItem::Named(_) => DocOwner::default(),
707 }
708}
709
710fn callable_owner(
711 _kind: DocTargetKind,
712 params: &[FunctionParameter],
713 type_params: Option<&[TypeParam]>,
714 return_type: Option<&TypeAnnotation>,
715) -> DocOwner {
716 DocOwner {
717 params: function_param_names(params),
718 type_params: type_param_names(type_params),
719 can_have_return_doc: !matches!(return_type, Some(TypeAnnotation::Void)),
720 }
721}
722
723fn type_owner(_kind: DocTargetKind, type_params: Option<&[TypeParam]>) -> DocOwner {
724 DocOwner {
725 type_params: type_param_names(type_params),
726 ..Default::default()
727 }
728}
729
730pub fn type_param_names(type_params: Option<&[TypeParam]>) -> Vec<String> {
731 type_params
733 .unwrap_or(&[])
734 .iter()
735 .map(|param| param.name().to_string())
736 .collect()
737}
738
739pub fn function_param_names(params: &[FunctionParameter]) -> Vec<String> {
740 let mut names = Vec::new();
741 for param in params {
742 names.extend(param.get_identifiers());
743 }
744 names.sort();
745 names.dedup();
746 names
747}
748
749fn trait_method_param_names(params: &[shape_ast::ast::FunctionParam]) -> Vec<String> {
750 let mut names = params
751 .iter()
752 .filter_map(|param| param.name.clone())
753 .collect::<Vec<_>>();
754 names.sort();
755 names.dedup();
756 names
757}
758
759fn join_path(prefix: &[String], name: &str) -> String {
760 if prefix.is_empty() {
761 name.to_string()
762 } else {
763 format!("{}::{}", prefix.join("::"), name)
764 }
765}
766
767fn join_annotation_path(prefix: &[String], name: &str) -> String {
768 join_path(prefix, &format!("@{name}"))
769}
770
771fn join_child_path(parent: &str, name: &str) -> String {
772 format!("{parent}::{name}")
773}
774
775fn join_type_param_path(parent: &str, name: &str) -> String {
776 format!("{parent}::<{name}>")
777}
778
779fn normalize_path(path: &Path) -> PathBuf {
780 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786 use shape_ast::parser::parse_program;
787
788 #[test]
789 fn collects_member_symbols_with_qualified_paths() {
790 let program = parse_program(
792 "type Point { /// x\n x: number }\ntrait Drawable { /// draw\n method draw(self) -> void;\n}\n",
793 )
794 .expect("program");
795 let symbols = collect_program_doc_symbols(&program, "pkg::math");
796 assert!(
797 symbols
798 .iter()
799 .any(|symbol| symbol.qualified_path == "pkg::math::Point::x")
800 );
801 assert!(
802 symbols
803 .iter()
804 .any(|symbol| symbol.qualified_path == "pkg::math::Drawable::draw")
805 );
806 }
807
808 #[test]
809 fn collects_annotation_symbols_with_canonical_paths() {
810 let program =
811 parse_program("/// Trace execution.\nannotation trace() {}\n").expect("program");
812 let symbols = collect_program_doc_symbols(&program, "pkg::debug");
813 assert!(
814 symbols
815 .iter()
816 .any(|symbol| symbol.qualified_path == "pkg::debug::@trace")
817 );
818 }
819}