use crate::analyzers::LanguageAnalyzer;
use crate::types::Result;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
pub trait LanguageAnalyzerRegistry: Send + Sync {
fn register(&mut self, analyzer: Box<dyn LanguageAnalyzer>) -> Result<()>;
fn get_by_language(&self, language: &str) -> Option<Arc<dyn LanguageAnalyzer>>;
fn get_by_extension(&self, extension: &str) -> Option<Arc<dyn LanguageAnalyzer>>;
fn detect_language(&self, file_path: &str, content: &str) -> Option<String>;
fn list_supported_languages(&self) -> Vec<String>;
fn list_supported_extensions(&self) -> Vec<String>;
}
#[derive(Clone)]
pub struct DefaultLanguageRegistry {
analyzers_by_language: Arc<RwLock<HashMap<String, Arc<dyn LanguageAnalyzer>>>>,
extensions_to_language: Arc<RwLock<HashMap<String, String>>>,
}
impl DefaultLanguageRegistry {
pub fn new() -> Self {
Self {
analyzers_by_language: Arc::new(RwLock::new(HashMap::new())),
extensions_to_language: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn get_analyzer_for_path(&self, file_path: &str) -> Option<Arc<dyn LanguageAnalyzer>> {
let extension = Self::extract_extension(file_path)?;
let language = self
.extensions_to_language
.read()
.ok()?
.get(&extension)
.cloned()?;
self.analyzers_by_language
.read()
.ok()?
.get(&language)
.cloned()
}
fn extract_extension(file_path: &str) -> Option<String> {
let path = std::path::Path::new(file_path);
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
if filename.starts_with('.') && filename.chars().filter(|&c| c == '.').count() == 1 {
return None;
}
}
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase())
}
}
impl Default for DefaultLanguageRegistry {
fn default() -> Self {
Self::new()
}
}
impl LanguageAnalyzerRegistry for DefaultLanguageRegistry {
fn register(&mut self, analyzer: Box<dyn LanguageAnalyzer>) -> Result<()> {
let language = analyzer.language().to_string();
let extensions = analyzer
.file_extensions()
.iter()
.map(|ext| ext.to_string())
.collect::<Vec<_>>();
{
let mut analyzers = self.analyzers_by_language.write().map_err(|_| {
crate::types::AnalysisError::RegistryError {
message: "Failed to acquire write lock for analyzers".to_string(),
}
})?;
if analyzers.contains_key(&language) {
return Err(crate::types::AnalysisError::RegistryError {
message: format!("Language '{}' is already registered", language),
});
}
analyzers.insert(language.clone(), Arc::from(analyzer));
}
{
let mut ext_map = self.extensions_to_language.write().map_err(|_| {
crate::types::AnalysisError::RegistryError {
message: "Failed to acquire write lock for extensions".to_string(),
}
})?;
for ext in extensions {
if ext_map.contains_key(&ext) {
return Err(crate::types::AnalysisError::RegistryError {
message: format!("Extension '{}' is already registered", ext),
});
}
ext_map.insert(ext, language.clone());
}
}
Ok(())
}
fn get_by_language(&self, language: &str) -> Option<Arc<dyn LanguageAnalyzer>> {
self.analyzers_by_language
.read()
.ok()?
.get(language)
.cloned()
}
fn get_by_extension(&self, extension: &str) -> Option<Arc<dyn LanguageAnalyzer>> {
let language = self
.extensions_to_language
.read()
.ok()?
.get(extension)
.cloned()?;
self.get_by_language(&language)
}
fn detect_language(&self, file_path: &str, _content: &str) -> Option<String> {
let extension = Self::extract_extension(file_path)?;
let ext_map = self.extensions_to_language.read().ok()?;
ext_map.get(&extension).cloned()
}
fn list_supported_languages(&self) -> Vec<String> {
if let Ok(analyzers) = self.analyzers_by_language.read() {
analyzers.keys().cloned().collect()
} else {
Vec::new()
}
}
fn list_supported_extensions(&self) -> Vec<String> {
if let Ok(ext_map) = self.extensions_to_language.read() {
ext_map.keys().cloned().collect()
} else {
Vec::new()
}
}
}
#[derive(Clone)]
pub struct RegistryHandle {
analyzers_by_language: Arc<RwLock<HashMap<String, Arc<dyn LanguageAnalyzer>>>>,
extensions_to_language: Arc<RwLock<HashMap<String, String>>>,
}
impl RegistryHandle {
pub fn new(registry: &DefaultLanguageRegistry) -> Self {
Self {
analyzers_by_language: registry.analyzers_by_language.clone(),
extensions_to_language: registry.extensions_to_language.clone(),
}
}
pub fn get_analyzer_by_language(&self, language: &str) -> Option<Arc<dyn LanguageAnalyzer>> {
self.analyzers_by_language
.read()
.ok()?
.get(language)
.cloned()
}
pub fn get_language_by_extension(&self, extension: &str) -> Option<String> {
if let Ok(ext_map) = self.extensions_to_language.read() {
ext_map.get(extension).cloned()
} else {
None
}
}
pub fn detect_language(&self, file_path: &str) -> Option<String> {
let extension = DefaultLanguageRegistry::extract_extension(file_path)?;
self.get_language_by_extension(&extension)
}
pub fn get_analyzer_for_path(&self, file_path: &str) -> Option<Arc<dyn LanguageAnalyzer>> {
let language = self.detect_language(file_path)?;
self.get_analyzer_by_language(&language)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyzers::rust::RustAnalyzer;
use crate::types::{
ExportStatement, FileAnalysis, FunctionCall, FunctionSignature, ImportStatement,
PartialAnalysis, StructSignature, TreeNode,
};
use async_trait::async_trait;
struct MockPythonAnalyzer;
#[async_trait]
impl LanguageAnalyzer for MockPythonAnalyzer {
fn language(&self) -> &'static str {
"python"
}
fn file_extensions(&self) -> &[&'static str] {
&["py", "pyx", "pyi"]
}
fn supports_async(&self) -> bool {
true
}
async fn analyze_file(&self, _content: &str, _file_path: &str) -> Result<FileAnalysis> {
Ok(FileAnalysis::new(
TreeNode::new("test.py".to_string(), "python".to_string()),
0,
))
}
fn extract_functions(
&self,
_tree: &tree_sitter::Tree,
_source: &str,
_file_path: &str,
) -> Result<Vec<FunctionSignature>> {
Ok(Vec::new())
}
fn extract_structs(
&self,
_tree: &tree_sitter::Tree,
_source: &str,
_file_path: &str,
) -> Result<Vec<StructSignature>> {
Ok(Vec::new())
}
fn extract_imports(
&self,
_tree: &tree_sitter::Tree,
_source: &str,
_file_path: &str,
) -> Result<Vec<ImportStatement>> {
Ok(Vec::new())
}
fn extract_exports(
&self,
_tree: &tree_sitter::Tree,
_source: &str,
_file_path: &str,
) -> Result<Vec<ExportStatement>> {
Ok(Vec::new())
}
fn extract_function_calls(
&self,
_tree: &tree_sitter::Tree,
_source: &str,
_file_path: &str,
) -> Result<Vec<FunctionCall>> {
Ok(Vec::new())
}
fn extract_with_fallback(&self, _content: &str, _file_path: &str) -> PartialAnalysis {
PartialAnalysis::new("test.py".to_string(), "python".to_string())
}
}
#[test]
fn test_registry_creation() {
let registry = DefaultLanguageRegistry::new();
assert_eq!(registry.list_supported_languages().len(), 0);
assert_eq!(registry.list_supported_extensions().len(), 0);
}
#[test]
fn test_register_analyzer() {
let mut registry = DefaultLanguageRegistry::new();
let analyzer = Box::new(MockPythonAnalyzer);
let result = registry.register(analyzer);
assert!(result.is_ok());
assert_eq!(registry.list_supported_languages().len(), 1);
assert_eq!(registry.list_supported_extensions().len(), 3);
assert!(
registry
.list_supported_languages()
.contains(&"python".to_string())
);
assert!(
registry
.list_supported_extensions()
.contains(&"py".to_string())
);
assert!(
registry
.list_supported_extensions()
.contains(&"pyx".to_string())
);
assert!(
registry
.list_supported_extensions()
.contains(&"pyi".to_string())
);
let by_lang = registry.get_by_language("python");
assert!(by_lang.is_some());
assert_eq!(by_lang.unwrap().language(), "python");
assert!(registry.get_by_extension("py").is_some());
assert!(registry.get_analyzer_for_path("module.pyx").is_some());
assert!(registry.get_by_language("rust").is_none());
assert!(registry.get_by_extension("rs").is_none());
assert!(registry.get_analyzer_for_path("main.rs").is_none());
}
#[test]
fn test_duplicate_language_registration() {
let mut registry = DefaultLanguageRegistry::new();
let analyzer1 = Box::new(MockPythonAnalyzer);
assert!(registry.register(analyzer1).is_ok());
let analyzer2 = Box::new(MockPythonAnalyzer);
let result = registry.register(analyzer2);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("already registered")
);
}
#[test]
fn test_language_detection() {
let mut registry = DefaultLanguageRegistry::new();
let analyzer = Box::new(MockPythonAnalyzer);
registry.register(analyzer).unwrap();
assert_eq!(
registry.detect_language("test.py", ""),
Some("python".to_string())
);
assert_eq!(
registry.detect_language("module.pyx", ""),
Some("python".to_string())
);
assert_eq!(
registry.detect_language("types.pyi", ""),
Some("python".to_string())
);
assert_eq!(registry.detect_language("test.rs", ""), None);
assert_eq!(registry.detect_language("README.md", ""), None);
assert_eq!(
registry.detect_language("Test.PY", ""),
Some("python".to_string())
);
assert_eq!(
registry.detect_language("MODULE.PYX", ""),
Some("python".to_string())
);
}
#[test]
fn test_registry_handle() {
let mut registry = DefaultLanguageRegistry::new();
let analyzer = Box::new(MockPythonAnalyzer);
registry.register(analyzer).unwrap();
let handle = RegistryHandle::new(®istry);
assert_eq!(
handle.detect_language("test.py"),
Some("python".to_string())
);
assert_eq!(handle.detect_language("test.rs"), None);
assert_eq!(
handle.get_language_by_extension("py"),
Some("python".to_string())
);
assert_eq!(handle.get_language_by_extension("rs"), None);
assert!(handle.get_analyzer_by_language("python").is_some());
assert!(handle.get_analyzer_by_language("rust").is_none());
}
#[test]
fn test_multiple_analyzers() {
let mut registry = DefaultLanguageRegistry::new();
let python_analyzer = Box::new(MockPythonAnalyzer);
assert!(registry.register(python_analyzer).is_ok());
let rust_analyzer = RustAnalyzer::new().unwrap();
let rust_boxed = Box::new(rust_analyzer) as Box<dyn LanguageAnalyzer>;
assert!(registry.register(rust_boxed).is_ok());
let languages = registry.list_supported_languages();
assert_eq!(languages.len(), 2);
assert!(languages.contains(&"python".to_string()));
assert!(languages.contains(&"rust".to_string()));
assert_eq!(
registry.detect_language("test.py", ""),
Some("python".to_string())
);
assert_eq!(
registry.detect_language("main.rs", ""),
Some("rust".to_string())
);
}
#[test]
fn test_extension_extraction() {
assert_eq!(
DefaultLanguageRegistry::extract_extension("test.py"),
Some("py".to_string())
);
assert_eq!(
DefaultLanguageRegistry::extract_extension("path/to/file.rs"),
Some("rs".to_string())
);
assert_eq!(
DefaultLanguageRegistry::extract_extension("FILE.PY"),
Some("py".to_string())
);
assert_eq!(
DefaultLanguageRegistry::extract_extension("no_extension"),
None
);
assert_eq!(DefaultLanguageRegistry::extract_extension(""), None);
assert_eq!(DefaultLanguageRegistry::extract_extension(".hidden"), None);
}
#[test]
fn test_empty_registry_operations() {
let registry = DefaultLanguageRegistry::new();
assert_eq!(registry.detect_language("test.py", ""), None);
assert_eq!(registry.list_supported_languages().len(), 0);
assert_eq!(registry.list_supported_extensions().len(), 0);
assert!(registry.get_by_language("python").is_none());
assert!(registry.get_by_extension("py").is_none());
}
}