use crate::doc_render::render_doc_comment;
use crate::module_cache::ModuleCache;
use shape_ast::ast::{AnnotationDef, DocComment, Item, Program, Span};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct AnnotationDiscovery {
local_annotations: HashMap<String, AnnotationInfo>,
imported_annotations: HashMap<String, AnnotationInfo>,
}
#[derive(Debug, Clone)]
pub struct AnnotationInfo {
pub name: String,
pub params: Vec<String>,
pub doc_comment: Option<DocComment>,
pub location: Span,
pub source_file: Option<std::path::PathBuf>,
source_program: Option<Arc<Program>>,
}
impl AnnotationDiscovery {
pub fn new() -> Self {
Self::default()
}
pub fn discover_from_program(&mut self, program: &Program) {
for item in &program.items {
if let Item::AnnotationDef(ann_def, _span) = item {
self.add_local_annotation(ann_def);
}
}
}
fn add_local_annotation(&mut self, ann_def: &AnnotationDef) {
let info = AnnotationInfo {
name: ann_def.name.clone(),
params: annotation_param_names(ann_def),
doc_comment: ann_def.doc_comment.clone(),
location: ann_def.name_span,
source_file: None, source_program: None,
};
self.local_annotations.insert(ann_def.name.clone(), info);
}
pub fn discover_from_imports_with_cache(
&mut self,
program: &Program,
current_file: &Path,
module_cache: &ModuleCache,
workspace_root: Option<&Path>,
) {
for item in &program.items {
if let Item::Import(import_stmt, _span) = item {
if let Some(module_path) =
module_cache.resolve_import(&import_stmt.from, current_file, workspace_root)
{
if let Some(module_info) = module_cache.load_module_with_context(
&module_path,
current_file,
workspace_root,
) {
self.discover_from_module_program(
module_info.program.clone(),
&module_info.path,
);
}
}
}
}
}
pub fn discover_from_imports(&mut self, _program: &Program) {
}
fn discover_from_module_program(
&mut self,
program: Arc<Program>,
source_path: &std::path::Path,
) {
for item in &program.items {
if let Item::AnnotationDef(ann_def, _span) = item {
let info = AnnotationInfo {
name: ann_def.name.clone(),
params: annotation_param_names(ann_def),
doc_comment: ann_def.doc_comment.clone(),
location: ann_def.name_span,
source_file: Some(source_path.to_path_buf()),
source_program: Some(program.clone()),
};
self.imported_annotations.insert(ann_def.name.clone(), info);
}
}
}
pub fn all_annotations(&self) -> Vec<&AnnotationInfo> {
self.local_annotations
.values()
.chain(self.imported_annotations.values())
.collect()
}
pub fn is_defined(&self, name: &str) -> bool {
self.local_annotations.contains_key(name) || self.imported_annotations.contains_key(name)
}
pub fn get(&self, name: &str) -> Option<&AnnotationInfo> {
self.local_annotations
.get(name)
.or_else(|| self.imported_annotations.get(name))
}
}
pub fn render_annotation_documentation(
info: &AnnotationInfo,
local_program: Option<&Program>,
module_cache: Option<&ModuleCache>,
current_file: Option<&Path>,
workspace_root: Option<&Path>,
) -> Option<String> {
let comment = info.doc_comment.as_ref()?;
let program = info.source_program.as_deref().or(local_program)?;
let source_file = info.source_file.as_deref().or(current_file);
Some(render_doc_comment(
program,
comment,
module_cache,
source_file,
workspace_root,
))
}
fn annotation_param_names(ann_def: &AnnotationDef) -> Vec<String> {
ann_def
.params
.iter()
.flat_map(|p| p.get_identifiers())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_discover_no_hardcoded_annotations() {
let mut discovery = AnnotationDiscovery::new();
discovery.discover_from_imports(&Program {
items: vec![],
docs: shape_ast::ast::ProgramDocs::default(),
});
assert!(!discovery.is_defined("strategy"));
assert!(!discovery.is_defined("export"));
assert!(!discovery.is_defined("warmup"));
assert!(!discovery.is_defined("undefined"));
}
#[test]
fn test_all_annotations_empty_without_imports() {
let mut discovery = AnnotationDiscovery::new();
discovery.discover_from_imports(&Program {
items: vec![],
docs: shape_ast::ast::ProgramDocs::default(),
});
let all = discovery.all_annotations();
assert_eq!(all.len(), 0);
}
#[test]
fn test_discover_with_module_cache_no_hardcoded() {
use crate::module_cache::ModuleCache;
use std::path::PathBuf;
let mut discovery = AnnotationDiscovery::new();
let module_cache = ModuleCache::new();
let current_file = PathBuf::from("/test/file.shape");
let program = Program {
items: vec![],
docs: shape_ast::ast::ProgramDocs::default(),
};
discovery.discover_from_imports_with_cache(&program, ¤t_file, &module_cache, None);
assert!(!discovery.is_defined("strategy"));
assert!(!discovery.is_defined("pattern"));
}
}