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 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 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, languages: languages.keys().cloned().collect(),
195 architecture_patterns: Vec::new(), };
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 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}