Skip to main content

shape_lsp/
doc_symbols.rs

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}