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 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 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 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}