1use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10use crate::config::Config;
11use crate::error::Result;
12use crate::file::FileInfo;
13use crate::types::{AnalysisResult, CentralityScores, RepositoryInfo, ScoreComponents};
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(
44 &self,
45 file_info: &FileInfo,
46 repo_info: &RepositoryInfo,
47 ) -> Result<ScoreComponents>;
48
49 fn score_batch(
51 &self,
52 files: &[&FileInfo],
53 repo_info: &RepositoryInfo,
54 ) -> Result<Vec<ScoreComponents>> {
55 files
56 .iter()
57 .map(|file| self.score_file(file, repo_info))
58 .collect()
59 }
60
61 fn name(&self) -> &'static str;
63
64 fn score_components(&self) -> Vec<&'static str>;
66
67 fn supports_advanced_features(&self) -> bool {
69 false
70 }
71}
72
73#[async_trait]
75pub trait RepositoryAnalyzer: Send + Sync {
76 async fn analyze_repository(&self, root_path: &Path, config: &Config)
78 -> Result<RepositoryInfo>;
79
80 async fn get_statistics(&self, root_path: &Path, files: &[FileInfo]) -> Result<RepositoryInfo>;
82
83 fn can_analyze(&self, root_path: &Path) -> bool;
85
86 fn priority(&self) -> u8 {
88 0
89 }
90}
91
92#[async_trait]
94pub trait GitIntegration: Send + Sync {
95 async fn is_git_repository(&self, path: &Path) -> Result<bool>;
97
98 async fn get_file_status(&self, files: &[&Path]) -> Result<Vec<crate::file::GitStatus>>;
100
101 async fn get_repo_info(&self, root_path: &Path) -> Result<GitRepositoryInfo>;
103
104 async fn analyze_churn(
106 &self,
107 root_path: &Path,
108 depth: usize,
109 ) -> Result<Vec<crate::types::ChurnInfo>>;
110
111 async fn should_ignore(&self, file_path: &Path, root_path: &Path) -> Result<bool>;
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct GitRepositoryInfo {
118 pub root: std::path::PathBuf,
120 pub branch: Option<String>,
122 pub remote_url: Option<String>,
124 pub last_commit: Option<String>,
126 pub has_changes: bool,
128}
129
130#[async_trait]
132pub trait CentralityComputer: Send + Sync {
133 async fn build_dependency_graph(&self, files: &[&FileInfo]) -> Result<DependencyGraph>;
135
136 async fn compute_centrality(&self, graph: &DependencyGraph) -> Result<CentralityScores>;
138
139 fn supported_languages(&self) -> Vec<crate::file::Language>;
141
142 fn can_analyze_file(&self, file_info: &FileInfo) -> bool;
144}
145
146#[derive(Debug, Clone)]
148pub struct DependencyGraph {
149 pub nodes: Vec<String>,
151
152 pub edges: Vec<Vec<usize>>,
154
155 pub reverse_edges: Vec<Vec<usize>>,
157
158 pub node_metadata: Vec<DependencyNodeMetadata>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct DependencyNodeMetadata {
165 pub path: String,
167 pub language: crate::file::Language,
169 pub size: u64,
171 pub is_test: bool,
173 pub is_entrypoint: bool,
175}
176
177impl DependencyGraph {
178 pub fn new() -> Self {
180 Self {
181 nodes: Vec::new(),
182 edges: Vec::new(),
183 reverse_edges: Vec::new(),
184 node_metadata: Vec::new(),
185 }
186 }
187
188 pub fn add_node(&mut self, path: String, metadata: DependencyNodeMetadata) -> usize {
190 let node_id = self.nodes.len();
191 self.nodes.push(path);
192 self.edges.push(Vec::new());
193 self.reverse_edges.push(Vec::new());
194 self.node_metadata.push(metadata);
195 node_id
196 }
197
198 pub fn add_edge(&mut self, source: usize, target: usize) {
200 if source < self.edges.len() && target < self.reverse_edges.len() {
201 self.edges[source].push(target);
202 self.reverse_edges[target].push(source);
203 }
204 }
205
206 pub fn stats(&self) -> crate::types::GraphStats {
208 let total_nodes = self.nodes.len();
209 let total_edges: usize = self.edges.iter().map(|adj| adj.len()).sum();
210
211 let in_degree_sum: usize = self.reverse_edges.iter().map(|adj| adj.len()).sum();
212 let out_degree_sum: usize = self.edges.iter().map(|adj| adj.len()).sum();
213
214 let in_degree_avg = if total_nodes > 0 {
215 in_degree_sum as f64 / total_nodes as f64
216 } else {
217 0.0
218 };
219 let out_degree_avg = if total_nodes > 0 {
220 out_degree_sum as f64 / total_nodes as f64
221 } else {
222 0.0
223 };
224
225 let in_degree_max = self
226 .reverse_edges
227 .iter()
228 .map(|adj| adj.len())
229 .max()
230 .unwrap_or(0);
231 let out_degree_max = self.edges.iter().map(|adj| adj.len()).max().unwrap_or(0);
232
233 let possible_edges = if total_nodes > 1 {
234 total_nodes * (total_nodes - 1)
235 } else {
236 0
237 };
238 let graph_density = if possible_edges > 0 {
239 total_edges as f64 / possible_edges as f64
240 } else {
241 0.0
242 };
243
244 crate::types::GraphStats {
245 total_nodes,
246 total_edges,
247 in_degree_avg,
248 in_degree_max,
249 out_degree_avg,
250 out_degree_max,
251 strongly_connected_components: 0, graph_density,
253 }
254 }
255}
256
257impl Default for DependencyGraph {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263pub trait PatternMatcher: Send + Sync {
265 fn matches(&self, path: &Path) -> bool;
267
268 fn pattern(&self) -> &str;
270
271 fn is_case_sensitive(&self) -> bool {
273 true
274 }
275}
276
277pub trait OutputFormatter: Send + Sync {
279 fn format_results(&self, results: &[AnalysisResult], config: &Config) -> Result<String>;
281
282 fn format_repository_info(&self, repo_info: &RepositoryInfo, config: &Config)
284 -> Result<String>;
285
286 fn format_name(&self) -> &'static str;
288
289 fn file_extension(&self) -> &'static str;
291
292 fn supports_streaming(&self) -> bool {
294 false
295 }
296}
297
298#[async_trait]
300pub trait CacheStorage: Send + Sync {
301 async fn get<T>(&self, key: &str) -> Result<Option<T>>
303 where
304 T: for<'de> Deserialize<'de> + Send;
305
306 async fn put<T>(&self, key: &str, value: &T, ttl: Option<std::time::Duration>) -> Result<()>
308 where
309 T: Serialize + Send + Sync;
310
311 async fn remove(&self, key: &str) -> Result<()>;
313
314 async fn clear(&self) -> Result<()>;
316
317 async fn stats(&self) -> Result<CacheStats>;
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct CacheStats {
324 pub item_count: usize,
326 pub size_bytes: u64,
328 pub hit_rate: f64,
330 pub hits: u64,
332 pub misses: u64,
334}
335
336pub trait ProgressReporter: Send + Sync {
338 fn start(&self, total: u64, message: &str);
340
341 fn update(&self, current: u64, message: Option<&str>);
343
344 fn finish(&self, message: &str);
346
347 fn error(&self, message: &str);
349
350 fn warning(&self, message: &str);
352
353 fn is_enabled(&self) -> bool;
355}
356
357#[async_trait]
359pub trait LanguageExtension: Send + Sync {
360 fn supported_languages(&self) -> Vec<crate::file::Language>;
362
363 async fn extract_dependencies(
365 &self,
366 content: &str,
367 language: crate::file::Language,
368 ) -> Result<Vec<String>>;
369
370 async fn is_entrypoint(&self, file_info: &FileInfo) -> Result<bool>;
372
373 async fn extract_documentation(
375 &self,
376 content: &str,
377 language: crate::file::Language,
378 ) -> Result<Vec<DocumentationBlock>>;
379
380 fn priority(&self) -> u8 {
382 0
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct DocumentationBlock {
389 pub text: String,
391 pub position: crate::types::Range,
393 pub doc_type: DocumentationType,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
399pub enum DocumentationType {
400 LineComment,
402 BlockComment,
404 DocComment,
406 Docstring,
408 ModuleDoc,
410 Readme,
412}
413
414pub trait PluginRegistry: Send + Sync {
416 fn register_analyzer(&mut self, analyzer: Box<dyn FileAnalyzer>);
418
419 fn register_scorer(&mut self, scorer: Box<dyn HeuristicScorer>);
421
422 fn register_repository_analyzer(&mut self, analyzer: Box<dyn RepositoryAnalyzer>);
424
425 fn register_formatter(&mut self, formatter: Box<dyn OutputFormatter>);
427
428 fn register_language_extension(&mut self, extension: Box<dyn LanguageExtension>);
430
431 fn get_analyzers(&self) -> Vec<&dyn FileAnalyzer>;
433
434 fn get_scorers(&self) -> Vec<&dyn HeuristicScorer>;
436
437 fn get_formatters(&self) -> Vec<&dyn OutputFormatter>;
439
440 fn load_plugins_from_dir(&mut self, dir: &Path) -> Result<usize>;
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[allow(dead_code)]
449 struct MockAnalyzer;
450
451 #[async_trait]
452 impl FileAnalyzer for MockAnalyzer {
453 async fn analyze_file(&self, _file_path: &Path, _config: &Config) -> Result<FileInfo> {
454 unimplemented!()
455 }
456
457 async fn analyze_content(&self, _file_info: &mut FileInfo, _config: &Config) -> Result<()> {
458 Ok(())
459 }
460
461 fn name(&self) -> &'static str {
462 "mock"
463 }
464
465 fn version(&self) -> &'static str {
466 "1.0.0"
467 }
468 }
469
470 #[test]
471 fn test_dependency_graph() {
472 let mut graph = DependencyGraph::new();
473
474 let metadata = DependencyNodeMetadata {
475 path: "test.rs".to_string(),
476 language: crate::file::Language::Rust,
477 size: 100,
478 is_test: false,
479 is_entrypoint: false,
480 };
481
482 let node1 = graph.add_node("file1.rs".to_string(), metadata.clone());
483 let node2 = graph.add_node("file2.rs".to_string(), metadata);
484
485 graph.add_edge(node1, node2);
486
487 assert_eq!(graph.nodes.len(), 2);
488 assert_eq!(graph.edges[node1].len(), 1);
489 assert_eq!(graph.reverse_edges[node2].len(), 1);
490
491 let stats = graph.stats();
492 assert_eq!(stats.total_nodes, 2);
493 assert_eq!(stats.total_edges, 1);
494 }
495
496 #[test]
497 fn test_documentation_block() {
498 use crate::types::{Position, Range};
499
500 let doc_block = DocumentationBlock {
501 text: "This is a test function".to_string(),
502 position: Range::new(Position::new(0, 0), Position::new(0, 23)),
503 doc_type: DocumentationType::DocComment,
504 };
505
506 assert_eq!(doc_block.doc_type, DocumentationType::DocComment);
507 assert!(doc_block.text.contains("test function"));
508 }
509}