1use crate::args::Args;
2use crate::core::{
3 ai_analyzer::{AIAnalyzer, AnalysisRequest, ProgressUpdate},
4 git::GitAnalyzer,
5 review::Review,
6};
7use anyhow::Result;
8use tokio::sync::mpsc;
9
10pub async fn perform_analysis_with_progress(
11 args: &Args,
12 progress_callback: Option<Box<dyn Fn(f64, String) + Send + Sync>>,
13) -> Result<Review> {
14 println!("📊 Starting AI-powered analysis...");
15
16 let git_analyzer = GitAnalyzer::new(&args.repo_path)?;
17
18 let changed_files = git_analyzer.get_changed_files(&args.source_branch, &args.target_branch)?;
20
21 println!("📈 Found {} changed files", changed_files.len());
22
23 let mut review = Review {
24 files_count: changed_files.len(),
25 issues_count: 0,
26 critical_issues: 0,
27 high_issues: 0,
28 medium_issues: 0,
29 low_issues: 0,
30 issues: Vec::new(),
31 };
32
33 let use_gpu = args.use_gpu && !args.force_cpu;
35 if args.force_cpu {
36 println!("💻 CPU mode forced by user with --cpu flag");
37 } else if args.use_gpu {
38 println!("🚀 GPU acceleration enabled (auto-detected or requested)");
39 }
40 let ai_analyzer = AIAnalyzer::new(use_gpu, !args.disable_ai).await?;
41
42 let (progress_tx, mut progress_rx) = mpsc::unbounded_channel::<ProgressUpdate>();
44
45 if let Some(callback) = progress_callback {
47 tokio::spawn(async move {
48 while let Some(update) = progress_rx.recv().await {
49 let status_message = if update.stage.is_empty() {
51 update.current_file
52 } else {
53 format!("{} - {}", update.current_file, update.stage)
54 };
55 callback(update.progress, status_message);
56 }
57 });
58 }
59
60 let total_files = changed_files.len() as f64;
62 for (index, file_path) in changed_files.iter().enumerate() {
63 if should_analyze_file(file_path, args) {
64 let commit_status = git_analyzer
65 .get_file_status(file_path)
66 .unwrap_or(crate::core::review::CommitStatus::Committed);
67
68 let status_indicator = match commit_status {
69 crate::core::review::CommitStatus::Committed => "📄",
70 crate::core::review::CommitStatus::Staged => "📑",
71 crate::core::review::CommitStatus::Modified => "📝",
72 crate::core::review::CommitStatus::Untracked => "📄",
73 };
74
75 let file_progress = (index as f64 / total_files) * 100.0;
76 println!(
77 " {status_indicator} Analyzing: {file_path} ({commit_status:?}) [{file_progress:.1}%]"
78 );
79
80 if let Ok(content) = git_analyzer.get_file_content(file_path, &args.target_branch) {
81 let request = AnalysisRequest {
82 file_path: file_path.clone(),
83 content,
84 language: detect_language(file_path),
85 commit_status,
86 };
87
88 match ai_analyzer
89 .analyze_file(request, Some(progress_tx.clone()))
90 .await
91 {
92 Ok(file_issues) => {
93 for issue in file_issues {
94 match issue.severity.as_str() {
95 "Critical" => review.critical_issues += 1,
96 "High" => review.high_issues += 1,
97 "Medium" => review.medium_issues += 1,
98 "Low" => review.low_issues += 1,
99 _ => {}
100 }
101 review.issues.push(issue);
102 review.issues_count += 1;
103 }
104 }
105 Err(e) => {
106 eprintln!("⚠️ Failed to analyze {file_path}: {e}");
107 }
108 }
109 }
110 }
111 }
112
113 drop(progress_tx);
115
116 println!(
117 "✅ AI analysis complete! Found {} issues.",
118 review.issues_count
119 );
120 Ok(review)
121}
122
123pub fn perform_analysis(args: &Args) -> Result<Review> {
124 let rt = tokio::runtime::Runtime::new()?;
126 rt.block_on(perform_analysis_with_progress(args, None))
127}
128
129fn should_analyze_file(file_path: &str, args: &Args) -> bool {
130 if !args.include_patterns.is_empty() {
132 let matches_include = args
133 .include_patterns
134 .iter()
135 .any(|pattern| file_matches_pattern(file_path, pattern));
136 if !matches_include {
137 return false;
138 }
139 }
140
141 for pattern in &args.exclude_patterns {
143 if file_matches_pattern(file_path, pattern) {
144 return false;
145 }
146 }
147
148 if file_path.starts_with("target/")
150 || file_path.contains("node_modules/")
151 || file_path.ends_with(".lock")
152 || file_path.ends_with(".log")
153 {
154 return false;
155 }
156
157 true
158}
159
160fn file_matches_pattern(file_path: &str, pattern: &str) -> bool {
161 if pattern.starts_with("*.") {
163 let extension = &pattern[1..];
164 file_path.ends_with(extension)
165 } else if let Some(prefix) = pattern.strip_suffix("/**") {
166 file_path.starts_with(prefix)
167 } else {
168 file_path.contains(pattern)
169 }
170}
171
172fn detect_language(file_path: &str) -> String {
173 use std::path::Path;
174 let path = Path::new(file_path);
175 match path.extension().and_then(|ext| ext.to_str()) {
176 Some("rs") => "rust".to_string(),
177 Some("js") => "javascript".to_string(),
178 Some("ts") => "typescript".to_string(),
179 Some("py") => "python".to_string(),
180 Some("java") => "java".to_string(),
181 Some("cpp") | Some("cc") | Some("cxx") => "cpp".to_string(),
182 Some("c") => "c".to_string(),
183 Some("go") => "go".to_string(),
184 Some("php") => "php".to_string(),
185 Some("rb") => "ruby".to_string(),
186 Some("cs") => "csharp".to_string(),
187 _ => "unknown".to_string(),
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 fn mk_args(include: Vec<&str>, exclude: Vec<&str>) -> Args {
196 Args {
197 repo_path: ".".to_string(),
198 source_branch: "main".to_string(),
199 target_branch: "HEAD".to_string(),
200 cli_mode: false,
201 verbose: false,
202 show_credits: false,
203 output_format: crate::args::OutputFormat::Summary,
204 include_patterns: include.into_iter().map(|s| s.to_string()).collect(),
205 exclude_patterns: exclude.into_iter().map(|s| s.to_string()).collect(),
206 use_gpu: false,
207 force_cpu: true,
208 disable_ai: false,
209 }
210 }
211
212 #[test]
213 fn test_file_matches_pattern_variants() {
214 assert!(file_matches_pattern("src/lib.rs", "*.rs"));
215 assert!(file_matches_pattern("src/core/mod.rs", "src/**"));
216 assert!(file_matches_pattern("foo/bar/baz.txt", "bar"));
217 assert!(!file_matches_pattern("src/lib.rs", "*.py"));
218 }
219
220 #[test]
221 fn test_should_analyze_file_include_exclude() {
222 let args = mk_args(vec!["*.rs"], vec![]);
224 assert!(should_analyze_file("src/lib.rs", &args));
225 assert!(!should_analyze_file("src/app.py", &args));
226
227 let args2 = mk_args(vec![], vec![]);
229 assert!(!should_analyze_file("target/debug/build.rs", &args2));
230 assert!(!should_analyze_file(
231 "foo/node_modules/pkg/index.js",
232 &args2
233 ));
234 assert!(!should_analyze_file("foo/app.log", &args2));
235 assert!(should_analyze_file("src/main.rs", &args2));
236
237 let args3 = mk_args(vec![], vec!["*.rs"]);
239 assert!(!should_analyze_file("src/lib.rs", &args3));
240 }
241
242 #[test]
243 fn test_detect_language_extensions() {
244 assert_eq!(detect_language("a.rs"), "rust");
245 assert_eq!(detect_language("a.js"), "javascript");
246 assert_eq!(detect_language("a.ts"), "typescript");
247 assert_eq!(detect_language("a.py"), "python");
248 assert_eq!(detect_language("a.unknown"), "unknown");
249 }
250}