shape_lsp/
annotation_discovery.rs1use 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#[derive(Debug, Clone, Default)]
14pub struct AnnotationDiscovery {
15 local_annotations: HashMap<String, AnnotationInfo>,
17 imported_annotations: HashMap<String, AnnotationInfo>,
19}
20
21#[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 pub source_file: Option<std::path::PathBuf>,
30 source_program: Option<Arc<Program>>,
31}
32
33impl AnnotationDiscovery {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 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 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, source_program: None,
57 };
58
59 self.local_annotations.insert(ann_def.name.clone(), info);
60 }
61
62 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 for item in &program.items {
80 if let Item::Import(import_stmt, _span) = item {
81 if let Some(module_path) =
83 module_cache.resolve_import(&import_stmt.from, current_file, workspace_root)
84 {
85 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 pub fn discover_from_imports(&mut self, _program: &Program) {
106 }
108
109 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 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 pub fn all_annotations(&self) -> Vec<&AnnotationInfo> {
133 self.local_annotations
134 .values()
135 .chain(self.imported_annotations.values())
136 .collect()
137 }
138
139 pub fn is_defined(&self, name: &str) -> bool {
141 self.local_annotations.contains_key(name) || self.imported_annotations.contains_key(name)
142 }
143
144 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 let mut discovery = AnnotationDiscovery::new();
187 discovery.discover_from_imports(&Program {
188 items: vec![],
189 docs: shape_ast::ast::ProgramDocs::default(),
190 });
191
192 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 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 let program = Program {
223 items: vec![],
224 docs: shape_ast::ast::ProgramDocs::default(),
225 };
226 discovery.discover_from_imports_with_cache(&program, ¤t_file, &module_cache, None);
227
228 assert!(!discovery.is_defined("strategy"));
231 assert!(!discovery.is_defined("pattern"));
232 }
233}