Skip to main content

shape_lsp/
doc_symbols.rs

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    // Shared accessor `name()` works across Type and Const variants.
732    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        // Fixture migrated to Form B per cd7d97a4 (2026-05-18) grammar surgery.
791        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}