Skip to main content

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 safe_truncate<'a>(&self, s: &'a str, max_chars: usize) -> &'a str {
209        if s.chars().count() <= max_chars {
210            return s;
211        }
212        
213        let mut end_idx = 0;
214        for (i, (idx, _)) in s.char_indices().enumerate() {
215            if i >= max_chars {
216                break;
217            }
218            end_idx = idx;
219        }
220        
221        // Find the next character boundary
222        if let Some((next_idx, _)) = s.char_indices().nth(max_chars) {
223            &s[..next_idx]
224        } else {
225            &s[..end_idx + s.chars().nth(max_chars.saturating_sub(1)).map_or(1, |c| c.len_utf8())]
226        }
227    }
228
229    fn extract_documentation_content(&self, files: &[FileInfo]) -> Vec<DocumentationContext> {
230        let mut documentation = Vec::new();
231        
232        for file in files {
233            if let Some(ref language) = file.language {
234                let is_documentation = matches!(language.as_str(), 
235                    "markdown" | "text" | "json" | "yaml" | "toml");
236                
237                if is_documentation {
238                    match fs::read_to_string(&file.path) {
239                        Ok(content) => {
240                            let summary = if content.chars().count() > 500 {
241                                format!("{}... ({} characters total)", 
242                                    self.safe_truncate(&content, 500), content.chars().count())
243                            } else {
244                                content.clone()
245                            };
246                            
247                            documentation.push(DocumentationContext {
248                                path: file.path.to_string_lossy().to_string(),
249                                file_type: language.clone(),
250                                content: if content.chars().count() > 8000 {
251                                    // Truncate very long files but keep first and last parts
252                                    let start_part = self.safe_truncate(&content, 4000);
253                                    let total_chars = content.chars().count();
254                                    let end_start = total_chars.saturating_sub(2000);
255                                    let end_part: String = content.chars().skip(end_start).collect();
256                                    
257                                    format!("{}...\n\n[FILE TRUNCATED - {} total characters]\n\n...{}", 
258                                        start_part, total_chars, end_part)
259                                } else {
260                                    content
261                                },
262                                summary,
263                            });
264                        }
265                        Err(e) => {
266                            eprintln!("Warning: Could not read documentation file {}: {}", 
267                                file.path.display(), e);
268                        }
269                    }
270                }
271            }
272        }
273        
274        documentation
275    }
276
277    fn create_prompt_for_type(&self, analysis_type: &AnalysisType) -> String {
278        match analysis_type {
279            AnalysisType::Overview => {
280                r#"Provide a comprehensive overview of this software project in the following JSON format:
281
282```json
283{
284  "analysis": "Brief overview of what the software does and its main purpose in 2-3 sentences",
285  "insights": [
286    {
287      "title": "Key Insight Title",
288      "description": "Detailed description of a key aspect, component, or characteristic of the project",
289      "category": "Architecture|Functionality|Technology|Implementation",
290      "confidence": 0.8,
291      "evidence": [
292        "Specific evidence from the codebase supporting this insight",
293        "Another piece of evidence"
294      ]
295    }
296  ],
297  "recommendations": [
298    {
299      "title": "Recommendation Title",
300      "description": "Detailed description of how to improve the project",
301      "priority": "High|Medium|Low",
302      "effort": "High|Medium|Low", 
303      "impact": "High|Medium|Low",
304      "action_items": [
305        "Specific actionable step",
306        "Another specific step"
307      ]
308    }
309  ],
310  "confidence": 0.8
311}
312```
313
314Focus 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()
315            }
316            AnalysisType::Architecture => {
317                r#"Analyze the software architecture of this project and provide insights in the following JSON format:
318
319```json
320{
321  "analysis": "Brief architectural overview of the project in 2-3 sentences",
322  "insights": [
323    {
324      "title": "Architecture Pattern Name",
325      "description": "Detailed description of the architectural pattern or design principle identified",
326      "category": "Architecture|Design Pattern|Structure|Organization",
327      "confidence": 0.8,
328      "evidence": [
329        "Specific evidence from the codebase supporting this insight",
330        "Another piece of evidence"
331      ]
332    }
333  ],
334  "recommendations": [
335    {
336      "title": "Recommendation Title",
337      "description": "Detailed description of the architectural improvement",
338      "priority": "High|Medium|Low",
339      "effort": "High|Medium|Low", 
340      "impact": "High|Medium|Low",
341      "action_items": [
342        "Specific actionable step",
343        "Another specific step"
344      ]
345    }
346  ],
347  "confidence": 0.8
348}
349```
350
351Focus 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()
352            }
353            AnalysisType::Dependencies => {
354                r#"Analyze the dependency relationships in this codebase and provide insights in the following JSON format:
355
356```json
357{
358  "analysis": "Brief summary of the dependency structure and key findings in 2-3 sentences",
359  "insights": [
360    {
361      "title": "Dependency Issue or Pattern Name",
362      "description": "Detailed description of the dependency pattern, coupling issue, or modularity aspect identified",
363      "category": "Coupling|Modularity|Dependencies|Structure",
364      "confidence": 0.8,
365      "evidence": [
366        "Specific evidence from the codebase supporting this insight",
367        "Another piece of evidence"
368      ]
369    }
370  ],
371  "recommendations": [
372    {
373      "title": "Recommendation Title",
374      "description": "Detailed description of how to improve dependency management or modularity",
375      "priority": "High|Medium|Low",
376      "effort": "High|Medium|Low", 
377      "impact": "High|Medium|Low",
378      "action_items": [
379        "Specific actionable step to improve dependencies",
380        "Another specific step"
381      ]
382    }
383  ],
384  "confidence": 0.8
385}
386```
387
388Focus 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()
389            }
390            AnalysisType::Security => {
391                "Perform a security analysis of this codebase. Look for potential vulnerabilities, insecure patterns, and provide security recommendations.".to_string()
392            }
393            AnalysisType::Refactoring => {
394                "Identify refactoring opportunities in this codebase. Look for code smells, duplication, and areas that could benefit from restructuring.".to_string()
395            }
396            AnalysisType::Documentation => {
397                "Generate comprehensive documentation for this software project, explaining how it works, its components, and usage patterns.".to_string()
398            }
399        }
400    }
401
402    pub fn get_file_summary(&self, files: &[FileInfo]) -> FileSummary {
403        let mut summary = FileSummary::default();
404        
405        for file in files {
406            summary.total_files += 1;
407            summary.total_size += file.size;
408            
409            if let Some(ref lang) = file.language {
410                *summary.language_distribution.entry(lang.clone()).or_insert(0) += 1;
411            }
412            
413            if let Some(ref ext) = file.extension {
414                *summary.extension_distribution.entry(ext.clone()).or_insert(0) += 1;
415            }
416        }
417        
418        summary
419    }
420
421    pub fn filter_files_by_criteria<'a>(&self, files: &'a [FileInfo], criteria: &FilterCriteria) -> Vec<&'a FileInfo> {
422        files.iter().filter(|file| {
423            if let Some(ref lang_filter) = criteria.language {
424                if file.language.as_ref() != Some(lang_filter) {
425                    return false;
426                }
427            }
428            
429            if let Some(min_size) = criteria.min_size {
430                if file.size < min_size {
431                    return false;
432                }
433            }
434            
435            if let Some(max_size) = criteria.max_size {
436                if file.size > max_size {
437                    return false;
438                }
439            }
440            
441            if let Some(ref path_contains) = criteria.path_contains {
442                if !file.path.to_string_lossy().contains(path_contains) {
443                    return false;
444                }
445            }
446            
447            true
448        }).collect()
449    }
450}
451
452#[derive(Debug, Serialize, Deserialize)]
453pub struct ProjectAnalysis {
454    pub files: Vec<FileInfo>,
455    pub parsed_files: Vec<ParsedFile>,
456    pub dependency_analysis: crate::dependency_graph::DependencyAnalysis,
457    pub llm_analysis: Vec<AnalysisResponse>,
458}
459
460impl ProjectAnalysis {
461    pub fn print_summary(&self) {
462        println!("πŸ“Š Project Analysis Summary");
463        println!("==========================");
464        
465        println!("\nπŸ“ Files:");
466        println!("  Total files: {}", self.files.len());
467        println!("  Successfully parsed: {}", self.parsed_files.len());
468        
469        println!("\nπŸ”— Dependencies:");
470        self.dependency_analysis.print_summary();
471        
472        println!("\nπŸ€– LLM Analysis:");
473        for (i, analysis) in self.llm_analysis.iter().enumerate() {
474            println!("  Analysis {}:", i + 1);
475            println!("    Confidence: {:.2}", analysis.confidence);
476            println!("    Insights: {}", analysis.insights.len());
477            println!("    Recommendations: {}", analysis.recommendations.len());
478        }
479    }
480
481    pub fn export_to_json(&self) -> Result<String> {
482        Ok(serde_json::to_string_pretty(self)?)
483    }
484}
485
486#[derive(Debug, Default, Clone, Serialize, Deserialize)]
487pub struct FileSummary {
488    pub total_files: usize,
489    pub total_size: u64,
490    pub language_distribution: HashMap<String, usize>,
491    pub extension_distribution: HashMap<String, usize>,
492}
493
494#[derive(Debug, Default)]
495pub struct FilterCriteria {
496    pub language: Option<String>,
497    pub min_size: Option<u64>,
498    pub max_size: Option<u64>,
499    pub path_contains: Option<String>,
500}