#![cfg_attr(coverage_nightly, coverage(off))]
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::Arc;
pub mod languages;
pub mod strategy;
#[cfg(any(feature = "c-ast", feature = "cpp-ast"))]
pub use languages::c_cpp_strategy;
use crate::services::context::FileContext;
use crate::services::file_classifier::FileClassifier;
#[async_trait]
pub trait AstStrategy: Send + Sync {
async fn analyze(&self, file_path: &Path, classifier: &FileClassifier) -> Result<FileContext>;
fn primary_extension(&self) -> &'static str;
fn supported_extensions(&self) -> Vec<&'static str>;
fn language_name(&self) -> &'static str;
fn can_handle(&self, extension: &str) -> bool {
self.supported_extensions().contains(&extension)
}
}
pub struct AstRegistry {
strategies: std::collections::HashMap<String, Arc<dyn AstStrategy>>,
}
impl AstRegistry {
#[must_use]
pub fn new() -> Self {
let mut registry = Self {
strategies: std::collections::HashMap::new(),
};
registry.register_defaults();
registry
}
fn register_defaults(&mut self) {
self.register(Arc::new(languages::rust::RustStrategy::new()));
#[cfg(feature = "typescript-ast")]
{
self.register(Arc::new(languages::typescript::TypeScriptStrategy::new()));
self.register(Arc::new(languages::javascript::JavaScriptStrategy::new()));
}
#[cfg(feature = "python-ast")]
{
self.register(Arc::new(languages::python::PythonStrategy::new()));
}
#[cfg(feature = "c-ast")]
{
self.register(Arc::new(languages::c_cpp_strategy::CStrategy::new()));
}
#[cfg(feature = "cpp-ast")]
{
self.register(Arc::new(languages::c_cpp_strategy::CppStrategy::new()));
}
#[cfg(feature = "kotlin-ast")]
{
self.register(Arc::new(languages::kotlin_strategy::KotlinStrategy::new()));
}
}
pub fn register(&mut self, strategy: Arc<dyn AstStrategy>) {
for ext in strategy.supported_extensions() {
self.strategies.insert(ext.to_string(), strategy.clone());
}
}
#[must_use]
pub fn get_strategy(&self, extension: &str) -> Option<Arc<dyn AstStrategy>> {
self.strategies.get(extension).cloned()
}
#[must_use]
pub fn list_supported_extensions(&self) -> Vec<&str> {
self.strategies
.keys()
.map(std::string::String::as_str)
.collect()
}
pub async fn analyze_file(
&self,
file_path: &Path,
classifier: &FileClassifier,
) -> Result<FileContext> {
let extension = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
if let Some(strategy) = self.get_strategy(extension) {
strategy.analyze(file_path, classifier).await
} else {
Ok(FileContext {
path: file_path.to_string_lossy().to_string(),
language: "Unknown".to_string(),
items: Vec::new(),
complexity_metrics: None,
})
}
}
}
impl Default for AstRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AstConfig {
pub include_complexity: bool,
pub include_functions: bool,
pub include_types: bool,
pub include_imports: bool,
pub max_depth: Option<usize>,
}
impl Default for AstConfig {
fn default() -> Self {
Self {
include_complexity: true,
include_functions: true,
include_types: true,
include_imports: true,
max_depth: None,
}
}
}
#[derive(Debug, Clone)]
pub struct AstAnalysisResult {
pub file_path: std::path::PathBuf,
pub language: String,
pub context: FileContext,
pub analysis_duration_ms: u64,
}
pub struct UnifiedAstAnalyzer {
registry: AstRegistry,
classifier: FileClassifier,
}
impl UnifiedAstAnalyzer {
#[must_use]
pub fn new() -> Self {
Self {
registry: AstRegistry::new(),
classifier: FileClassifier::default(),
}
}
pub async fn analyze_file(&self, file_path: &Path) -> Result<AstAnalysisResult> {
let start = std::time::Instant::now();
let context = self
.registry
.analyze_file(file_path, &self.classifier)
.await?;
let language = context.language.clone();
let duration = start.elapsed();
Ok(AstAnalysisResult {
file_path: file_path.to_path_buf(),
language,
context,
analysis_duration_ms: std::cmp::max(1, duration.as_millis() as u64),
})
}
#[must_use]
pub fn supported_languages(&self) -> Vec<&str> {
self.registry.list_supported_extensions()
}
}
impl Default for UnifiedAstAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_ast_registry_creation() {
let registry = AstRegistry::new();
let extensions = registry.list_supported_extensions();
assert!(extensions.contains(&"rs"));
assert!(!extensions.is_empty());
}
#[tokio::test]
async fn test_rust_strategy() {
let registry = AstRegistry::new();
let rust_strategy = registry.get_strategy("rs").unwrap();
assert_eq!(rust_strategy.primary_extension(), "rs");
assert_eq!(rust_strategy.language_name(), "Rust");
assert!(rust_strategy.can_handle("rs"));
}
#[tokio::test]
async fn test_unified_analyzer() {
let analyzer = UnifiedAstAnalyzer::new();
let languages = analyzer.supported_languages();
assert!(languages.contains(&"rs"));
assert!(!languages.is_empty());
}
#[tokio::test]
async fn test_file_analysis() {
let mut temp_file = NamedTempFile::new().unwrap();
temp_file
.write_all(b"fn main() { println!(\"Hello, world!\"); }")
.unwrap();
temp_file.flush().unwrap();
let rust_file_path = temp_file.path().with_extension("rs");
std::fs::copy(temp_file.path(), &rust_file_path).unwrap();
let analyzer = UnifiedAstAnalyzer::new();
let result = analyzer.analyze_file(&rust_file_path).await.unwrap();
assert_eq!(result.language, "rust");
assert!(result.analysis_duration_ms > 0);
assert_eq!(result.file_path, rust_file_path);
std::fs::remove_file(&rust_file_path).unwrap();
}
#[test]
fn test_ast_config_default() {
let config = AstConfig::default();
assert!(config.include_complexity);
assert!(config.include_functions);
assert!(config.include_types);
assert!(config.include_imports);
assert!(config.max_depth.is_none());
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod additional_tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_ast_config_default() {
let config = AstConfig::default();
assert!(config.include_complexity);
assert!(config.include_functions);
assert!(config.include_types);
assert!(config.include_imports);
assert!(config.max_depth.is_none());
}
#[test]
fn test_ast_config_custom() {
let config = AstConfig {
include_complexity: false,
include_functions: true,
include_types: false,
include_imports: true,
max_depth: Some(10),
};
assert!(!config.include_complexity);
assert!(config.include_functions);
assert!(!config.include_types);
assert!(config.include_imports);
assert_eq!(config.max_depth, Some(10));
}
#[test]
fn test_ast_registry_creation() {
let registry = AstRegistry::new();
let extensions = registry.list_supported_extensions();
assert!(extensions.contains(&"rs"));
assert!(!extensions.is_empty());
}
#[test]
fn test_ast_registry_get_strategy() {
let registry = AstRegistry::new();
let rust_strategy = registry.get_strategy("rs");
assert!(rust_strategy.is_some());
let unknown_strategy = registry.get_strategy("unknown");
assert!(unknown_strategy.is_none());
}
#[tokio::test]
async fn test_unified_ast_analyzer_creation() {
let analyzer = UnifiedAstAnalyzer::new();
let languages = analyzer.supported_languages();
assert!(!languages.is_empty());
}
#[test]
fn test_unified_ast_analyzer_creation_sync() {
let analyzer = UnifiedAstAnalyzer::new();
let languages = analyzer.supported_languages();
assert!(languages.contains(&"rs"));
}
#[tokio::test]
async fn test_analyze_file_with_analyzer() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(&test_file, "fn test() -> i32 { 42 }").unwrap();
let analyzer = UnifiedAstAnalyzer::new();
let result = analyzer.analyze_file(&test_file).await;
match result {
Ok(analysis) => {
assert_eq!(analysis.language, "rust");
assert!(analysis.analysis_duration_ms > 0);
}
Err(_) => {
}
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}