use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::config::Config;
use crate::error::Result;
use crate::file::FileInfo;
use crate::types::{AnalysisResult, CentralityScores, RepositoryInfo, ScoreComponents};
#[async_trait]
pub trait FileAnalyzer: Send + Sync {
async fn analyze_file(&self, file_path: &Path, config: &Config) -> Result<FileInfo>;
async fn analyze_content(&self, file_info: &mut FileInfo, config: &Config) -> Result<()>;
async fn analyze_batch(&self, files: Vec<&Path>, config: &Config) -> Result<Vec<FileInfo>> {
let mut results = Vec::new();
for file in files {
results.push(self.analyze_file(file, config).await?);
}
Ok(results)
}
fn name(&self) -> &'static str;
fn version(&self) -> &'static str;
}
pub trait HeuristicScorer: Send + Sync {
fn score_file(
&self,
file_info: &FileInfo,
repo_info: &RepositoryInfo,
) -> Result<ScoreComponents>;
fn score_batch(
&self,
files: &[&FileInfo],
repo_info: &RepositoryInfo,
) -> Result<Vec<ScoreComponents>> {
files
.iter()
.map(|file| self.score_file(file, repo_info))
.collect()
}
fn name(&self) -> &'static str;
fn score_components(&self) -> Vec<&'static str>;
fn supports_advanced_features(&self) -> bool {
false
}
}
#[async_trait]
pub trait RepositoryAnalyzer: Send + Sync {
async fn analyze_repository(&self, root_path: &Path, config: &Config)
-> Result<RepositoryInfo>;
async fn get_statistics(&self, root_path: &Path, files: &[FileInfo]) -> Result<RepositoryInfo>;
fn can_analyze(&self, root_path: &Path) -> bool;
fn priority(&self) -> u8 {
0
}
}
#[async_trait]
pub trait GitIntegration: Send + Sync {
async fn is_git_repository(&self, path: &Path) -> Result<bool>;
async fn get_file_status(&self, files: &[&Path]) -> Result<Vec<crate::file::GitStatus>>;
async fn get_repo_info(&self, root_path: &Path) -> Result<GitRepositoryInfo>;
async fn analyze_churn(
&self,
root_path: &Path,
depth: usize,
) -> Result<Vec<crate::types::ChurnInfo>>;
async fn should_ignore(&self, file_path: &Path, root_path: &Path) -> Result<bool>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitRepositoryInfo {
pub root: std::path::PathBuf,
pub branch: Option<String>,
pub remote_url: Option<String>,
pub last_commit: Option<String>,
pub has_changes: bool,
}
#[async_trait]
pub trait CentralityComputer: Send + Sync {
async fn build_dependency_graph(&self, files: &[&FileInfo]) -> Result<DependencyGraph>;
async fn compute_centrality(&self, graph: &DependencyGraph) -> Result<CentralityScores>;
fn supported_languages(&self) -> Vec<crate::file::Language>;
fn can_analyze_file(&self, file_info: &FileInfo) -> bool;
}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
pub nodes: Vec<String>,
pub edges: Vec<Vec<usize>>,
pub reverse_edges: Vec<Vec<usize>>,
pub node_metadata: Vec<DependencyNodeMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyNodeMetadata {
pub path: String,
pub language: crate::file::Language,
pub size: u64,
pub is_test: bool,
pub is_entrypoint: bool,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
reverse_edges: Vec::new(),
node_metadata: Vec::new(),
}
}
pub fn add_node(&mut self, path: String, metadata: DependencyNodeMetadata) -> usize {
let node_id = self.nodes.len();
self.nodes.push(path);
self.edges.push(Vec::new());
self.reverse_edges.push(Vec::new());
self.node_metadata.push(metadata);
node_id
}
pub fn add_edge(&mut self, source: usize, target: usize) {
if source < self.edges.len() && target < self.reverse_edges.len() {
self.edges[source].push(target);
self.reverse_edges[target].push(source);
}
}
pub fn stats(&self) -> crate::types::GraphStats {
let total_nodes = self.nodes.len();
let total_edges: usize = self.edges.iter().map(|adj| adj.len()).sum();
let in_degree_sum: usize = self.reverse_edges.iter().map(|adj| adj.len()).sum();
let out_degree_sum: usize = self.edges.iter().map(|adj| adj.len()).sum();
let in_degree_avg = if total_nodes > 0 {
in_degree_sum as f64 / total_nodes as f64
} else {
0.0
};
let out_degree_avg = if total_nodes > 0 {
out_degree_sum as f64 / total_nodes as f64
} else {
0.0
};
let in_degree_max = self
.reverse_edges
.iter()
.map(|adj| adj.len())
.max()
.unwrap_or(0);
let out_degree_max = self.edges.iter().map(|adj| adj.len()).max().unwrap_or(0);
let possible_edges = if total_nodes > 1 {
total_nodes * (total_nodes - 1)
} else {
0
};
let graph_density = if possible_edges > 0 {
total_edges as f64 / possible_edges as f64
} else {
0.0
};
crate::types::GraphStats {
total_nodes,
total_edges,
in_degree_avg,
in_degree_max,
out_degree_avg,
out_degree_max,
strongly_connected_components: 0, graph_density,
}
}
}
impl Default for DependencyGraph {
fn default() -> Self {
Self::new()
}
}
pub trait PatternMatcher: Send + Sync {
fn matches(&self, path: &Path) -> bool;
fn pattern(&self) -> &str;
fn is_case_sensitive(&self) -> bool {
true
}
}
pub trait OutputFormatter: Send + Sync {
fn format_results(&self, results: &[AnalysisResult], config: &Config) -> Result<String>;
fn format_repository_info(&self, repo_info: &RepositoryInfo, config: &Config)
-> Result<String>;
fn format_name(&self) -> &'static str;
fn file_extension(&self) -> &'static str;
fn supports_streaming(&self) -> bool {
false
}
}
#[async_trait]
pub trait CacheStorage: Send + Sync {
async fn get<T>(&self, key: &str) -> Result<Option<T>>
where
T: for<'de> Deserialize<'de> + Send;
async fn put<T>(&self, key: &str, value: &T, ttl: Option<std::time::Duration>) -> Result<()>
where
T: Serialize + Send + Sync;
async fn remove(&self, key: &str) -> Result<()>;
async fn clear(&self) -> Result<()>;
async fn stats(&self) -> Result<CacheStats>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStats {
pub item_count: usize,
pub size_bytes: u64,
pub hit_rate: f64,
pub hits: u64,
pub misses: u64,
}
pub trait ProgressReporter: Send + Sync {
fn start(&self, total: u64, message: &str);
fn update(&self, current: u64, message: Option<&str>);
fn finish(&self, message: &str);
fn error(&self, message: &str);
fn warning(&self, message: &str);
fn is_enabled(&self) -> bool;
}
#[async_trait]
pub trait LanguageExtension: Send + Sync {
fn supported_languages(&self) -> Vec<crate::file::Language>;
async fn extract_dependencies(
&self,
content: &str,
language: crate::file::Language,
) -> Result<Vec<String>>;
async fn is_entrypoint(&self, file_info: &FileInfo) -> Result<bool>;
async fn extract_documentation(
&self,
content: &str,
language: crate::file::Language,
) -> Result<Vec<DocumentationBlock>>;
fn priority(&self) -> u8 {
0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentationBlock {
pub text: String,
pub position: crate::types::Range,
pub doc_type: DocumentationType,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DocumentationType {
LineComment,
BlockComment,
DocComment,
Docstring,
ModuleDoc,
Readme,
}
pub trait PluginRegistry: Send + Sync {
fn register_analyzer(&mut self, analyzer: Box<dyn FileAnalyzer>);
fn register_scorer(&mut self, scorer: Box<dyn HeuristicScorer>);
fn register_repository_analyzer(&mut self, analyzer: Box<dyn RepositoryAnalyzer>);
fn register_formatter(&mut self, formatter: Box<dyn OutputFormatter>);
fn register_language_extension(&mut self, extension: Box<dyn LanguageExtension>);
fn get_analyzers(&self) -> Vec<&dyn FileAnalyzer>;
fn get_scorers(&self) -> Vec<&dyn HeuristicScorer>;
fn get_formatters(&self) -> Vec<&dyn OutputFormatter>;
fn load_plugins_from_dir(&mut self, dir: &Path) -> Result<usize>;
}
#[cfg(test)]
mod tests {
use super::*;
struct MockAnalyzer;
#[async_trait]
impl FileAnalyzer for MockAnalyzer {
async fn analyze_file(&self, _file_path: &Path, _config: &Config) -> Result<FileInfo> {
unimplemented!()
}
async fn analyze_content(&self, _file_info: &mut FileInfo, _config: &Config) -> Result<()> {
Ok(())
}
fn name(&self) -> &'static str {
"mock"
}
fn version(&self) -> &'static str {
"1.0.0"
}
}
#[test]
fn mock_analyzer_metadata() {
let analyzer = MockAnalyzer;
assert_eq!(analyzer.name(), "mock");
assert_eq!(analyzer.version(), "1.0.0");
}
#[test]
fn test_dependency_graph() {
let mut graph = DependencyGraph::new();
let metadata = DependencyNodeMetadata {
path: "test.rs".to_string(),
language: crate::file::Language::Rust,
size: 100,
is_test: false,
is_entrypoint: false,
};
let node1 = graph.add_node("file1.rs".to_string(), metadata.clone());
let node2 = graph.add_node("file2.rs".to_string(), metadata);
graph.add_edge(node1, node2);
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges[node1].len(), 1);
assert_eq!(graph.reverse_edges[node2].len(), 1);
let stats = graph.stats();
assert_eq!(stats.total_nodes, 2);
assert_eq!(stats.total_edges, 1);
}
#[test]
fn test_documentation_block() {
use crate::types::{Position, Range};
let doc_block = DocumentationBlock {
text: "This is a test function".to_string(),
position: Range::new(Position::new(0, 0), Position::new(0, 23)),
doc_type: DocumentationType::DocComment,
};
assert_eq!(doc_block.doc_type, DocumentationType::DocComment);
assert!(doc_block.text.contains("test function"));
}
}