Skip to main content

shape_lsp/
annotation_discovery.rs

1//! Annotation discovery for LSP
2//!
3//! Dynamically discovers user-defined annotations from AST and imported modules.
4
5use crate::doc_render::render_doc_comment;
6use crate::module_cache::ModuleCache;
7use shape_ast::ast::{AnnotationDef, DocComment, Item, Program, Span};
8use std::collections::HashMap;
9use std::path::Path;
10use std::sync::Arc;
11
12/// Discovers annotations from program AST and imports
13#[derive(Debug, Clone, Default)]
14pub struct AnnotationDiscovery {
15    /// Annotations defined in current file
16    local_annotations: HashMap<String, AnnotationInfo>,
17    /// Annotations from imports (stdlib, user modules)
18    imported_annotations: HashMap<String, AnnotationInfo>,
19}
20
21/// Information about a discovered annotation
22#[derive(Debug, Clone)]
23pub struct AnnotationInfo {
24    pub name: String,
25    pub params: Vec<String>,
26    pub doc_comment: Option<DocComment>,
27    pub location: Span,
28    /// Source file where the annotation is defined (None for local annotations)
29    pub source_file: Option<std::path::PathBuf>,
30    source_program: Option<Arc<Program>>,
31}
32
33impl AnnotationDiscovery {
34    /// Create a new annotation discovery instance
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Discover annotations from a parsed program
40    pub fn discover_from_program(&mut self, program: &Program) {
41        for item in &program.items {
42            if let Item::AnnotationDef(ann_def, _span) = item {
43                self.add_local_annotation(ann_def);
44            }
45        }
46    }
47
48    /// Add a locally-defined annotation
49    fn add_local_annotation(&mut self, ann_def: &AnnotationDef) {
50        let info = AnnotationInfo {
51            name: ann_def.name.clone(),
52            params: annotation_param_names(ann_def),
53            doc_comment: ann_def.doc_comment.clone(),
54            location: ann_def.name_span,
55            source_file: None, // Local annotations are in the current file
56            source_program: None,
57        };
58
59        self.local_annotations.insert(ann_def.name.clone(), info);
60    }
61
62    /// Discover annotations from imported modules using module cache
63    ///
64    /// Looks at import statements in the program and loads the corresponding
65    /// modules to discover their exported annotations.
66    ///
67    /// NOTE: Annotations are now fully defined in Shape stdlib, not hardcoded in Rust.
68    /// The LSP discovers annotations from:
69    /// 1. Local `annotation ... { ... }` definitions in the current file
70    /// 2. Imported modules (including stdlib/core/annotations/, stdlib/finance/, etc.)
71    pub fn discover_from_imports_with_cache(
72        &mut self,
73        program: &Program,
74        current_file: &Path,
75        module_cache: &ModuleCache,
76        workspace_root: Option<&Path>,
77    ) {
78        // Discover from imports - annotations are defined in stdlib modules
79        for item in &program.items {
80            if let Item::Import(import_stmt, _span) = item {
81                // Try to resolve the import path (from field contains the module path)
82                if let Some(module_path) =
83                    module_cache.resolve_import(&import_stmt.from, current_file, workspace_root)
84                {
85                    // Load the module and discover its annotations
86                    if let Some(module_info) = module_cache.load_module_with_context(
87                        &module_path,
88                        current_file,
89                        workspace_root,
90                    ) {
91                        self.discover_from_module_program(
92                            module_info.program.clone(),
93                            &module_info.path,
94                        );
95                    }
96                }
97            }
98        }
99    }
100
101    /// Discover annotations from imported modules (simple version without module cache)
102    ///
103    /// NOTE: Without module cache, no annotations are discovered.
104    /// Use discover_from_imports_with_cache() for full annotation discovery.
105    pub fn discover_from_imports(&mut self, _program: &Program) {
106        // Annotations are defined in stdlib modules - no hardcoded builtins
107    }
108
109    /// Discover annotations from an already-loaded module program
110    fn discover_from_module_program(
111        &mut self,
112        program: Arc<Program>,
113        source_path: &std::path::Path,
114    ) {
115        for item in &program.items {
116            if let Item::AnnotationDef(ann_def, _span) = item {
117                // Add the annotation to imported_annotations
118                let info = AnnotationInfo {
119                    name: ann_def.name.clone(),
120                    params: annotation_param_names(ann_def),
121                    doc_comment: ann_def.doc_comment.clone(),
122                    location: ann_def.name_span,
123                    source_file: Some(source_path.to_path_buf()),
124                    source_program: Some(program.clone()),
125                };
126                self.imported_annotations.insert(ann_def.name.clone(), info);
127            }
128        }
129    }
130
131    /// Get all discovered annotations
132    pub fn all_annotations(&self) -> Vec<&AnnotationInfo> {
133        self.local_annotations
134            .values()
135            .chain(self.imported_annotations.values())
136            .collect()
137    }
138
139    /// Check if an annotation is defined
140    pub fn is_defined(&self, name: &str) -> bool {
141        self.local_annotations.contains_key(name) || self.imported_annotations.contains_key(name)
142    }
143
144    /// Get information about a specific annotation
145    pub fn get(&self, name: &str) -> Option<&AnnotationInfo> {
146        self.local_annotations
147            .get(name)
148            .or_else(|| self.imported_annotations.get(name))
149    }
150}
151
152pub fn render_annotation_documentation(
153    info: &AnnotationInfo,
154    local_program: Option<&Program>,
155    module_cache: Option<&ModuleCache>,
156    current_file: Option<&Path>,
157    workspace_root: Option<&Path>,
158) -> Option<String> {
159    let comment = info.doc_comment.as_ref()?;
160    let program = info.source_program.as_deref().or(local_program)?;
161    let source_file = info.source_file.as_deref().or(current_file);
162    Some(render_doc_comment(
163        program,
164        comment,
165        module_cache,
166        source_file,
167        workspace_root,
168    ))
169}
170
171fn annotation_param_names(ann_def: &AnnotationDef) -> Vec<String> {
172    ann_def
173        .params
174        .iter()
175        .flat_map(|p| p.get_identifiers())
176        .collect()
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_discover_no_hardcoded_annotations() {
185        // Annotations are now discovered from imports, not hardcoded
186        let mut discovery = AnnotationDiscovery::new();
187        discovery.discover_from_imports(&Program {
188            items: vec![],
189            docs: shape_ast::ast::ProgramDocs::default(),
190        });
191
192        // With no imports, no annotations should be defined
193        assert!(!discovery.is_defined("strategy"));
194        assert!(!discovery.is_defined("export"));
195        assert!(!discovery.is_defined("warmup"));
196        assert!(!discovery.is_defined("undefined"));
197    }
198
199    #[test]
200    fn test_all_annotations_empty_without_imports() {
201        // Without imports, annotation list should be empty
202        let mut discovery = AnnotationDiscovery::new();
203        discovery.discover_from_imports(&Program {
204            items: vec![],
205            docs: shape_ast::ast::ProgramDocs::default(),
206        });
207
208        let all = discovery.all_annotations();
209        assert_eq!(all.len(), 0);
210    }
211
212    #[test]
213    fn test_discover_with_module_cache_no_hardcoded() {
214        use crate::module_cache::ModuleCache;
215        use std::path::PathBuf;
216
217        let mut discovery = AnnotationDiscovery::new();
218        let module_cache = ModuleCache::new();
219        let current_file = PathBuf::from("/test/file.shape");
220
221        // Without actual module imports, no annotations should be discovered
222        let program = Program {
223            items: vec![],
224            docs: shape_ast::ast::ProgramDocs::default(),
225        };
226        discovery.discover_from_imports_with_cache(&program, &current_file, &module_cache, None);
227
228        // Annotations are now defined in stdlib, not hardcoded
229        // With no imports, none should be available
230        assert!(!discovery.is_defined("strategy"));
231        assert!(!discovery.is_defined("pattern"));
232    }
233}