1use std::path::Path;
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9
10use crate::error::Result;
11use crate::file::FileInfo;
12use crate::types::{AnalysisResult, ScoreComponents, RepositoryInfo, CentralityScores};
13use crate::config::Config;
14
15#[async_trait]
17pub trait FileAnalyzer: Send + Sync {
18 async fn analyze_file(&self, file_path: &Path, config: &Config) -> Result<FileInfo>;
20
21 async fn analyze_content(&self, file_info: &mut FileInfo, config: &Config) -> Result<()>;
23
24 async fn analyze_batch(&self, files: Vec<&Path>, config: &Config) -> Result<Vec<FileInfo>> {
26 let mut results = Vec::new();
27 for file in files {
28 results.push(self.analyze_file(file, config).await?);
29 }
30 Ok(results)
31 }
32
33 fn name(&self) -> &'static str;
35
36 fn version(&self) -> &'static str;
38}
39
40pub trait HeuristicScorer: Send + Sync {
42 fn score_file(&self, file_info: &FileInfo, repo_info: &RepositoryInfo) -> Result<ScoreComponents>;
44
45 fn score_batch(&self, files: &[&FileInfo], repo_info: &RepositoryInfo) -> Result<Vec<ScoreComponents>> {
47 files.iter()
48 .map(|file| self.score_file(file, repo_info))
49 .collect()
50 }
51
52 fn name(&self) -> &'static str;
54
55 fn score_components(&self) -> Vec<&'static str>;
57
58 fn supports_advanced_features(&self) -> bool {
60 false
61 }
62}
63
64#[async_trait]
66pub trait RepositoryAnalyzer: Send + Sync {
67 async fn analyze_repository(&self, root_path: &Path, config: &Config) -> Result<RepositoryInfo>;
69
70 async fn get_statistics(&self, root_path: &Path, files: &[FileInfo]) -> Result<RepositoryInfo>;
72
73 fn can_analyze(&self, root_path: &Path) -> bool;
75
76 fn priority(&self) -> u8 {
78 0
79 }
80}
81
82#[async_trait]
84pub trait GitIntegration: Send + Sync {
85 async fn is_git_repository(&self, path: &Path) -> Result<bool>;
87
88 async fn get_file_status(&self, files: &[&Path]) -> Result<Vec<crate::file::GitStatus>>;
90
91 async fn get_repo_info(&self, root_path: &Path) -> Result<GitRepositoryInfo>;
93
94 async fn analyze_churn(&self, root_path: &Path, depth: usize) -> Result<Vec<crate::types::ChurnInfo>>;
96
97 async fn should_ignore(&self, file_path: &Path, root_path: &Path) -> Result<bool>;
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct GitRepositoryInfo {
104 pub root: std::path::PathBuf,
106 pub branch: Option<String>,
108 pub remote_url: Option<String>,
110 pub last_commit: Option<String>,
112 pub has_changes: bool,
114}
115
116#[async_trait]
118pub trait CentralityComputer: Send + Sync {
119 async fn build_dependency_graph(&self, files: &[&FileInfo]) -> Result<DependencyGraph>;
121
122 async fn compute_centrality(&self, graph: &DependencyGraph) -> Result<CentralityScores>;
124
125 fn supported_languages(&self) -> Vec<crate::file::Language>;
127
128 fn can_analyze_file(&self, file_info: &FileInfo) -> bool;
130}
131
132#[derive(Debug, Clone)]
134pub struct DependencyGraph {
135 pub nodes: Vec<String>,
137
138 pub edges: Vec<Vec<usize>>,
140
141 pub reverse_edges: Vec<Vec<usize>>,
143
144 pub node_metadata: Vec<DependencyNodeMetadata>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct DependencyNodeMetadata {
151 pub path: String,
153 pub language: crate::file::Language,
155 pub size: u64,
157 pub is_test: bool,
159 pub is_entrypoint: bool,
161}
162
163impl DependencyGraph {
164 pub fn new() -> Self {
166 Self {
167 nodes: Vec::new(),
168 edges: Vec::new(),
169 reverse_edges: Vec::new(),
170 node_metadata: Vec::new(),
171 }
172 }
173
174 pub fn add_node(&mut self, path: String, metadata: DependencyNodeMetadata) -> usize {
176 let node_id = self.nodes.len();
177 self.nodes.push(path);
178 self.edges.push(Vec::new());
179 self.reverse_edges.push(Vec::new());
180 self.node_metadata.push(metadata);
181 node_id
182 }
183
184 pub fn add_edge(&mut self, source: usize, target: usize) {
186 if source < self.edges.len() && target < self.reverse_edges.len() {
187 self.edges[source].push(target);
188 self.reverse_edges[target].push(source);
189 }
190 }
191
192 pub fn stats(&self) -> crate::types::GraphStats {
194 let total_nodes = self.nodes.len();
195 let total_edges: usize = self.edges.iter().map(|adj| adj.len()).sum();
196
197 let in_degree_sum: usize = self.reverse_edges.iter().map(|adj| adj.len()).sum();
198 let out_degree_sum: usize = self.edges.iter().map(|adj| adj.len()).sum();
199
200 let in_degree_avg = if total_nodes > 0 { in_degree_sum as f64 / total_nodes as f64 } else { 0.0 };
201 let out_degree_avg = if total_nodes > 0 { out_degree_sum as f64 / total_nodes as f64 } else { 0.0 };
202
203 let in_degree_max = self.reverse_edges.iter().map(|adj| adj.len()).max().unwrap_or(0);
204 let out_degree_max = self.edges.iter().map(|adj| adj.len()).max().unwrap_or(0);
205
206 let possible_edges = if total_nodes > 1 { total_nodes * (total_nodes - 1) } else { 0 };
207 let graph_density = if possible_edges > 0 { total_edges as f64 / possible_edges as f64 } else { 0.0 };
208
209 crate::types::GraphStats {
210 total_nodes,
211 total_edges,
212 in_degree_avg,
213 in_degree_max,
214 out_degree_avg,
215 out_degree_max,
216 strongly_connected_components: 0, graph_density,
218 }
219 }
220}
221
222impl Default for DependencyGraph {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228pub trait PatternMatcher: Send + Sync {
230 fn matches(&self, path: &Path) -> bool;
232
233 fn pattern(&self) -> &str;
235
236 fn is_case_sensitive(&self) -> bool {
238 true
239 }
240}
241
242pub trait OutputFormatter: Send + Sync {
244 fn format_results(&self, results: &[AnalysisResult], config: &Config) -> Result<String>;
246
247 fn format_repository_info(&self, repo_info: &RepositoryInfo, config: &Config) -> Result<String>;
249
250 fn format_name(&self) -> &'static str;
252
253 fn file_extension(&self) -> &'static str;
255
256 fn supports_streaming(&self) -> bool {
258 false
259 }
260}
261
262#[async_trait]
264pub trait CacheStorage: Send + Sync {
265 async fn get<T>(&self, key: &str) -> Result<Option<T>>
267 where
268 T: for<'de> Deserialize<'de> + Send;
269
270 async fn put<T>(&self, key: &str, value: &T, ttl: Option<std::time::Duration>) -> Result<()>
272 where
273 T: Serialize + Send + Sync;
274
275 async fn remove(&self, key: &str) -> Result<()>;
277
278 async fn clear(&self) -> Result<()>;
280
281 async fn stats(&self) -> Result<CacheStats>;
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct CacheStats {
288 pub item_count: usize,
290 pub size_bytes: u64,
292 pub hit_rate: f64,
294 pub hits: u64,
296 pub misses: u64,
298}
299
300pub trait ProgressReporter: Send + Sync {
302 fn start(&self, total: u64, message: &str);
304
305 fn update(&self, current: u64, message: Option<&str>);
307
308 fn finish(&self, message: &str);
310
311 fn error(&self, message: &str);
313
314 fn warning(&self, message: &str);
316
317 fn is_enabled(&self) -> bool;
319}
320
321#[async_trait]
323pub trait LanguageExtension: Send + Sync {
324 fn supported_languages(&self) -> Vec<crate::file::Language>;
326
327 async fn extract_dependencies(&self, content: &str, language: crate::file::Language) -> Result<Vec<String>>;
329
330 async fn is_entrypoint(&self, file_info: &FileInfo) -> Result<bool>;
332
333 async fn extract_documentation(&self, content: &str, language: crate::file::Language) -> Result<Vec<DocumentationBlock>>;
335
336 fn priority(&self) -> u8 {
338 0
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct DocumentationBlock {
345 pub text: String,
347 pub position: crate::types::Range,
349 pub doc_type: DocumentationType,
351}
352
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
355pub enum DocumentationType {
356 LineComment,
358 BlockComment,
360 DocComment,
362 Docstring,
364 ModuleDoc,
366 Readme,
368}
369
370pub trait PluginRegistry: Send + Sync {
372 fn register_analyzer(&mut self, analyzer: Box<dyn FileAnalyzer>);
374
375 fn register_scorer(&mut self, scorer: Box<dyn HeuristicScorer>);
377
378 fn register_repository_analyzer(&mut self, analyzer: Box<dyn RepositoryAnalyzer>);
380
381 fn register_formatter(&mut self, formatter: Box<dyn OutputFormatter>);
383
384 fn register_language_extension(&mut self, extension: Box<dyn LanguageExtension>);
386
387 fn get_analyzers(&self) -> Vec<&dyn FileAnalyzer>;
389
390 fn get_scorers(&self) -> Vec<&dyn HeuristicScorer>;
392
393 fn get_formatters(&self) -> Vec<&dyn OutputFormatter>;
395
396 fn load_plugins_from_dir(&mut self, dir: &Path) -> Result<usize>;
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[allow(dead_code)]
405 struct MockAnalyzer;
406
407 #[async_trait]
408 impl FileAnalyzer for MockAnalyzer {
409 async fn analyze_file(&self, _file_path: &Path, _config: &Config) -> Result<FileInfo> {
410 unimplemented!()
411 }
412
413 async fn analyze_content(&self, _file_info: &mut FileInfo, _config: &Config) -> Result<()> {
414 Ok(())
415 }
416
417 fn name(&self) -> &'static str {
418 "mock"
419 }
420
421 fn version(&self) -> &'static str {
422 "1.0.0"
423 }
424 }
425
426 #[test]
427 fn test_dependency_graph() {
428 let mut graph = DependencyGraph::new();
429
430 let metadata = DependencyNodeMetadata {
431 path: "test.rs".to_string(),
432 language: crate::file::Language::Rust,
433 size: 100,
434 is_test: false,
435 is_entrypoint: false,
436 };
437
438 let node1 = graph.add_node("file1.rs".to_string(), metadata.clone());
439 let node2 = graph.add_node("file2.rs".to_string(), metadata);
440
441 graph.add_edge(node1, node2);
442
443 assert_eq!(graph.nodes.len(), 2);
444 assert_eq!(graph.edges[node1].len(), 1);
445 assert_eq!(graph.reverse_edges[node2].len(), 1);
446
447 let stats = graph.stats();
448 assert_eq!(stats.total_nodes, 2);
449 assert_eq!(stats.total_edges, 1);
450 }
451
452 #[test]
453 fn test_documentation_block() {
454 use crate::types::{Position, Range};
455
456 let doc_block = DocumentationBlock {
457 text: "This is a test function".to_string(),
458 position: Range::new(Position::new(0, 0), Position::new(0, 23)),
459 doc_type: DocumentationType::DocComment,
460 };
461
462 assert_eq!(doc_block.doc_type, DocumentationType::DocComment);
463 assert!(doc_block.text.contains("test function"));
464 }
465}