ai_code_buddy/core/
analysis.rs

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    // Get changed files between branches
19    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    // Initialize AI analyzer
34    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).await?;
41
42    // Create progress channel
43    let (progress_tx, mut progress_rx) = mpsc::unbounded_channel::<ProgressUpdate>();
44
45    // Spawn task to handle progress updates
46    if let Some(callback) = progress_callback {
47        tokio::spawn(async move {
48            while let Some(update) = progress_rx.recv().await {
49                // Format the current file with stage information
50                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    // Analyze each file
61    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    // Close progress channel
114    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    // Create a simple runtime for synchronous callers
125    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    // Check include patterns
131    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    // Check exclude patterns
142    for pattern in &args.exclude_patterns {
143        if file_matches_pattern(file_path, pattern) {
144            return false;
145        }
146    }
147
148    // Default exclusions
149    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    // Simple pattern matching - can be enhanced with glob
162    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        }
209    }
210
211    #[test]
212    fn test_file_matches_pattern_variants() {
213        assert!(file_matches_pattern("src/lib.rs", "*.rs"));
214        assert!(file_matches_pattern("src/core/mod.rs", "src/**"));
215        assert!(file_matches_pattern("foo/bar/baz.txt", "bar"));
216        assert!(!file_matches_pattern("src/lib.rs", "*.py"));
217    }
218
219    #[test]
220    fn test_should_analyze_file_include_exclude() {
221        // Include only rs
222        let args = mk_args(vec!["*.rs"], vec![]);
223        assert!(should_analyze_file("src/lib.rs", &args));
224        assert!(!should_analyze_file("src/app.py", &args));
225
226        // Exclude target and logs by default
227        let args2 = mk_args(vec![], vec![]);
228        assert!(!should_analyze_file("target/debug/build.rs", &args2));
229        assert!(!should_analyze_file(
230            "foo/node_modules/pkg/index.js",
231            &args2
232        ));
233        assert!(!should_analyze_file("foo/app.log", &args2));
234        assert!(should_analyze_file("src/main.rs", &args2));
235
236        // Explicit exclude wins
237        let args3 = mk_args(vec![], vec!["*.rs"]);
238        assert!(!should_analyze_file("src/lib.rs", &args3));
239    }
240
241    #[test]
242    fn test_detect_language_extensions() {
243        assert_eq!(detect_language("a.rs"), "rust");
244        assert_eq!(detect_language("a.js"), "javascript");
245        assert_eq!(detect_language("a.ts"), "typescript");
246        assert_eq!(detect_language("a.py"), "python");
247        assert_eq!(detect_language("a.unknown"), "unknown");
248    }
249}