project_examer/
analyzer.rs

1use crate::{
2    config::Config,
3    dependency_graph::{DependencyGraph, GraphBuilder},
4    file_discovery::{FileDiscovery, FileInfo},
5    llm::{AnalysisRequest, AnalysisContext, AnalysisType, FileContext, DependencyContext, ProjectInfo, LLMClient, AnalysisResponse, DocumentationContext},
6    simple_parser::{SimpleParser, ParsedFile},
7};
8use anyhow::Result;
9use rayon::prelude::*;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fs;
13
14pub struct Analyzer {
15    config: Config,
16    file_discovery: FileDiscovery,
17    llm_client: LLMClient,
18}
19
20impl Analyzer {
21    pub fn new(config: Config, debug_llm: bool) -> Result<Self> {
22        let file_discovery = FileDiscovery::new(config.clone());
23        let llm_client = LLMClient::new(config.llm.clone(), debug_llm);
24
25        Ok(Self {
26            config,
27            file_discovery,
28            llm_client,
29        })
30    }
31
32    pub async fn analyze_project(&mut self, skip_llm: bool) -> Result<ProjectAnalysis> {
33        println!("πŸ” Discovering files...");
34        let files = self.file_discovery.discover_files()?;
35        let stats = self.file_discovery.get_stats(&files);
36        stats.print_summary();
37
38        println!("\nπŸ“ Parsing files...");
39        let parsed_files = self.parse_files_parallel(&files)?;
40
41        println!("\nπŸ•ΈοΈ  Building dependency graph...");
42        let mut graph_builder = GraphBuilder::new();
43        let graph = graph_builder.build_graph(&parsed_files);
44        
45        // Clone the graph and get analysis before using in async function
46        let graph_copy = graph.clone();
47        let graph_analysis = graph_builder.analyze_dependencies();
48        graph_analysis.print_summary();
49
50        let llm_analysis = if skip_llm {
51            println!("\n⚑ Skipping LLM analysis (local-only mode)");
52            Vec::new()
53        } else {
54            println!("\nπŸ€– Analyzing with LLM...");
55            self.analyze_with_llm(&parsed_files, &graph_copy, &files).await?
56        };
57
58        Ok(ProjectAnalysis {
59            files: files.clone(),
60            parsed_files,
61            dependency_analysis: graph_analysis,
62            llm_analysis,
63        })
64    }
65
66    fn parse_files_parallel(&mut self, files: &[FileInfo]) -> Result<Vec<ParsedFile>> {
67        let chunk_size = std::cmp::max(1, files.len() / rayon::current_num_threads());
68        
69        Ok(files
70            .par_chunks(chunk_size)
71            .map(|chunk| {
72                let local_parser = SimpleParser::new().unwrap();
73                let mut parsed_files = Vec::new();
74                
75                for file_info in chunk {
76                    match local_parser.parse_file(file_info) {
77                        Ok(parsed_file) => {
78                            println!("  βœ“ {}", file_info.path.display());
79                            parsed_files.push(parsed_file);
80                        }
81                        Err(e) => {
82                            eprintln!("  βœ— {}: {}", file_info.path.display(), e);
83                        }
84                    }
85                }
86                
87                parsed_files
88            })
89            .reduce(Vec::new, |mut acc, mut chunk| {
90                acc.append(&mut chunk);
91                acc
92            }))
93    }
94
95    async fn analyze_with_llm(
96        &self,
97        parsed_files: &[ParsedFile],
98        _graph: &DependencyGraph,
99        files: &[FileInfo],
100    ) -> Result<Vec<AnalysisResponse>> {
101        println!("  πŸ“Š Preparing analysis context...");
102        let context = self.create_analysis_context(parsed_files, _graph, files);
103        
104        let analysis_types = vec![
105            ("Overview", AnalysisType::Overview),
106            ("Architecture", AnalysisType::Architecture), 
107            ("Dependencies", AnalysisType::Dependencies),
108        ];
109
110        println!("  πŸ”„ Running {} analysis types...", analysis_types.len());
111        
112        let mut results = Vec::new();
113        for (i, (name, analysis_type)) in analysis_types.iter().enumerate() {
114            println!("  {} Analyzing {} ({}/{})...", 
115                if i == 0 { "πŸš€" } else { "πŸ“ˆ" }, 
116                name, 
117                i + 1, 
118                analysis_types.len()
119            );
120            
121            let prompt = self.create_prompt_for_type(analysis_type);
122            let request = AnalysisRequest {
123                prompt,
124                context: context.clone(),
125                analysis_type: analysis_type.clone(),
126            };
127
128            match self.llm_client.analyze(request).await {
129                Ok(response) => {
130                    println!("    βœ… {} analysis completed", name);
131                    results.push(response);
132                }
133                Err(e) => {
134                    println!("    ⚠️  {} analysis failed: {}", name, e);
135                    // Continue with other analyses even if one fails
136                    println!("    πŸ“ Continuing with remaining analyses...");
137                }
138            }
139        }
140
141        if results.is_empty() {
142            println!("  ⚠️  All LLM analyses failed, continuing with local analysis only");
143        } else {
144            println!("  βœ… Completed {}/{} LLM analyses successfully", results.len(), analysis_types.len());
145        }
146
147        Ok(results)
148    }
149
150    fn create_analysis_context(
151        &self,
152        parsed_files: &[ParsedFile],
153        _graph: &DependencyGraph,
154        files: &[FileInfo],
155    ) -> AnalysisContext {
156        let file_contexts: Vec<FileContext> = parsed_files.iter().map(|pf| {
157            FileContext {
158                path: pf.file_info.path.to_string_lossy().to_string(),
159                language: pf.file_info.language.clone().unwrap_or_else(|| "unknown".to_string()),
160                content_summary: format!("{} functions, {} classes, {} imports", 
161                    pf.functions.len(), pf.classes.len(), pf.imports.len()),
162                functions: pf.functions.iter().map(|f| f.name.clone()).collect(),
163                classes: pf.classes.iter().map(|c| c.name.clone()).collect(),
164                imports: pf.imports.iter().map(|i| i.module.clone()).collect(),
165            }
166        }).collect();
167
168        let dependency_contexts: Vec<DependencyContext> = parsed_files.iter().flat_map(|pf| {
169            pf.imports.iter().map(|import| {
170                DependencyContext {
171                    from_file: pf.file_info.path.to_string_lossy().to_string(),
172                    to_file: import.module.clone(),
173                    dependency_type: "import".to_string(),
174                    strength: 1.0,
175                }
176            })
177        }).collect();
178
179        let mut languages = HashMap::new();
180        for file in files {
181            if let Some(ref lang) = file.language {
182                *languages.entry(lang.clone()).or_insert(0) += 1;
183            }
184        }
185
186        let project_info = ProjectInfo {
187            name: self.config.target_directory
188                .file_name()
189                .and_then(|n| n.to_str())
190                .unwrap_or("unknown")
191                .to_string(),
192            total_files: files.len(),
193            total_lines: files.iter().map(|f| f.size as usize).sum::<usize>() / 50, // Rough estimate
194            languages: languages.keys().cloned().collect(),
195            architecture_patterns: Vec::new(), // Will be filled by analysis
196        };
197
198        let documentation = self.extract_documentation_content(files);
199
200        AnalysisContext {
201            files: file_contexts,
202            dependencies: dependency_contexts,
203            project_info,
204            documentation,
205        }
206    }
207
208    fn extract_documentation_content(&self, files: &[FileInfo]) -> Vec<DocumentationContext> {
209        let mut documentation = Vec::new();
210        
211        for file in files {
212            if let Some(ref language) = file.language {
213                let is_documentation = matches!(language.as_str(), 
214                    "markdown" | "text" | "json" | "yaml" | "toml");
215                
216                if is_documentation {
217                    match fs::read_to_string(&file.path) {
218                        Ok(content) => {
219                            let summary = if content.len() > 500 {
220                                format!("{}... ({} characters total)", &content[..500], content.len())
221                            } else {
222                                content.clone()
223                            };
224                            
225                            documentation.push(DocumentationContext {
226                                path: file.path.to_string_lossy().to_string(),
227                                file_type: language.clone(),
228                                content: if content.len() > 8000 {
229                                    // Truncate very long files but keep first and last parts
230                                    format!("{}...\n\n[FILE TRUNCATED - {} total characters]\n\n...{}", 
231                                        &content[..4000], content.len(), &content[content.len().saturating_sub(2000)..])
232                                } else {
233                                    content
234                                },
235                                summary,
236                            });
237                        }
238                        Err(e) => {
239                            eprintln!("Warning: Could not read documentation file {}: {}", 
240                                file.path.display(), e);
241                        }
242                    }
243                }
244            }
245        }
246        
247        documentation
248    }
249
250    fn create_prompt_for_type(&self, analysis_type: &AnalysisType) -> String {
251        match analysis_type {
252            AnalysisType::Overview => {
253                r#"Provide a comprehensive overview of this software project in the following JSON format:
254
255```json
256{
257  "analysis": "Brief overview of what the software does and its main purpose in 2-3 sentences",
258  "insights": [
259    {
260      "title": "Key Insight Title",
261      "description": "Detailed description of a key aspect, component, or characteristic of the project",
262      "category": "Architecture|Functionality|Technology|Implementation",
263      "confidence": 0.8,
264      "evidence": [
265        "Specific evidence from the codebase supporting this insight",
266        "Another piece of evidence"
267      ]
268    }
269  ],
270  "recommendations": [
271    {
272      "title": "Recommendation Title",
273      "description": "Detailed description of how to improve the project",
274      "priority": "High|Medium|Low",
275      "effort": "High|Medium|Low", 
276      "impact": "High|Medium|Low",
277      "action_items": [
278        "Specific actionable step",
279        "Another specific step"
280      ]
281    }
282  ],
283  "confidence": 0.8
284}
285```
286
287Focus on describing what the software does, its main components, technology choices, architecture style, and how different parts work together. Use the provided documentation files (README, configuration files, etc.) to understand the project's purpose, goals, and design decisions."#.to_string()
288            }
289            AnalysisType::Architecture => {
290                r#"Analyze the software architecture of this project and provide insights in the following JSON format:
291
292```json
293{
294  "analysis": "Brief architectural overview of the project in 2-3 sentences",
295  "insights": [
296    {
297      "title": "Architecture Pattern Name",
298      "description": "Detailed description of the architectural pattern or design principle identified",
299      "category": "Architecture|Design Pattern|Structure|Organization",
300      "confidence": 0.8,
301      "evidence": [
302        "Specific evidence from the codebase supporting this insight",
303        "Another piece of evidence"
304      ]
305    }
306  ],
307  "recommendations": [
308    {
309      "title": "Recommendation Title",
310      "description": "Detailed description of the architectural improvement",
311      "priority": "High|Medium|Low",
312      "effort": "High|Medium|Low", 
313      "impact": "High|Medium|Low",
314      "action_items": [
315        "Specific actionable step",
316        "Another specific step"
317      ]
318    }
319  ],
320  "confidence": 0.8
321}
322```
323
324Focus on identifying architectural patterns (MVC, microservices, layered, etc.), design principles (SOLID, DRY, etc.), structural organization, modularity, and provide actionable recommendations for architectural improvements. Use the provided documentation to understand the intended architecture and design decisions."#.to_string()
325            }
326            AnalysisType::Dependencies => {
327                r#"Analyze the dependency relationships in this codebase and provide insights in the following JSON format:
328
329```json
330{
331  "analysis": "Brief summary of the dependency structure and key findings in 2-3 sentences",
332  "insights": [
333    {
334      "title": "Dependency Issue or Pattern Name",
335      "description": "Detailed description of the dependency pattern, coupling issue, or modularity aspect identified",
336      "category": "Coupling|Modularity|Dependencies|Structure",
337      "confidence": 0.8,
338      "evidence": [
339        "Specific evidence from the codebase supporting this insight",
340        "Another piece of evidence"
341      ]
342    }
343  ],
344  "recommendations": [
345    {
346      "title": "Recommendation Title",
347      "description": "Detailed description of how to improve dependency management or modularity",
348      "priority": "High|Medium|Low",
349      "effort": "High|Medium|Low", 
350      "impact": "High|Medium|Low",
351      "action_items": [
352        "Specific actionable step to improve dependencies",
353        "Another specific step"
354      ]
355    }
356  ],
357  "confidence": 0.8
358}
359```
360
361Focus on identifying coupling issues, circular dependencies, modularity problems, dependency injection opportunities, and provide actionable recommendations for better dependency management. Consider the project's documentation to understand intended module relationships and design goals."#.to_string()
362            }
363            AnalysisType::Security => {
364                "Perform a security analysis of this codebase. Look for potential vulnerabilities, insecure patterns, and provide security recommendations.".to_string()
365            }
366            AnalysisType::Refactoring => {
367                "Identify refactoring opportunities in this codebase. Look for code smells, duplication, and areas that could benefit from restructuring.".to_string()
368            }
369            AnalysisType::Documentation => {
370                "Generate comprehensive documentation for this software project, explaining how it works, its components, and usage patterns.".to_string()
371            }
372        }
373    }
374
375    pub fn get_file_summary(&self, files: &[FileInfo]) -> FileSummary {
376        let mut summary = FileSummary::default();
377        
378        for file in files {
379            summary.total_files += 1;
380            summary.total_size += file.size;
381            
382            if let Some(ref lang) = file.language {
383                *summary.language_distribution.entry(lang.clone()).or_insert(0) += 1;
384            }
385            
386            if let Some(ref ext) = file.extension {
387                *summary.extension_distribution.entry(ext.clone()).or_insert(0) += 1;
388            }
389        }
390        
391        summary
392    }
393
394    pub fn filter_files_by_criteria<'a>(&self, files: &'a [FileInfo], criteria: &FilterCriteria) -> Vec<&'a FileInfo> {
395        files.iter().filter(|file| {
396            if let Some(ref lang_filter) = criteria.language {
397                if file.language.as_ref() != Some(lang_filter) {
398                    return false;
399                }
400            }
401            
402            if let Some(min_size) = criteria.min_size {
403                if file.size < min_size {
404                    return false;
405                }
406            }
407            
408            if let Some(max_size) = criteria.max_size {
409                if file.size > max_size {
410                    return false;
411                }
412            }
413            
414            if let Some(ref path_contains) = criteria.path_contains {
415                if !file.path.to_string_lossy().contains(path_contains) {
416                    return false;
417                }
418            }
419            
420            true
421        }).collect()
422    }
423}
424
425#[derive(Debug, Serialize, Deserialize)]
426pub struct ProjectAnalysis {
427    pub files: Vec<FileInfo>,
428    pub parsed_files: Vec<ParsedFile>,
429    pub dependency_analysis: crate::dependency_graph::DependencyAnalysis,
430    pub llm_analysis: Vec<AnalysisResponse>,
431}
432
433impl ProjectAnalysis {
434    pub fn print_summary(&self) {
435        println!("πŸ“Š Project Analysis Summary");
436        println!("==========================");
437        
438        println!("\nπŸ“ Files:");
439        println!("  Total files: {}", self.files.len());
440        println!("  Successfully parsed: {}", self.parsed_files.len());
441        
442        println!("\nπŸ”— Dependencies:");
443        self.dependency_analysis.print_summary();
444        
445        println!("\nπŸ€– LLM Analysis:");
446        for (i, analysis) in self.llm_analysis.iter().enumerate() {
447            println!("  Analysis {}:", i + 1);
448            println!("    Confidence: {:.2}", analysis.confidence);
449            println!("    Insights: {}", analysis.insights.len());
450            println!("    Recommendations: {}", analysis.recommendations.len());
451        }
452    }
453
454    pub fn export_to_json(&self) -> Result<String> {
455        Ok(serde_json::to_string_pretty(self)?)
456    }
457}
458
459#[derive(Debug, Default, Clone, Serialize, Deserialize)]
460pub struct FileSummary {
461    pub total_files: usize,
462    pub total_size: u64,
463    pub language_distribution: HashMap<String, usize>,
464    pub extension_distribution: HashMap<String, usize>,
465}
466
467#[derive(Debug, Default)]
468pub struct FilterCriteria {
469    pub language: Option<String>,
470    pub min_size: Option<u64>,
471    pub max_size: Option<u64>,
472    pub path_contains: Option<String>,
473}