1use colored::Colorize;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use textwrap;
5
6const EXPLANATION_WRAP_WIDTH: usize = 80;
8
9#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
11pub struct CodeIssue {
12 pub description: String,
14 pub severity: String,
16 pub location: String,
19 pub explanation: String,
21 pub recommendation: String,
23}
24
25#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
27pub struct DimensionAnalysis {
28 pub issues_found: bool,
30 pub issues: Vec<CodeIssue>,
32}
33
34#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
36pub enum QualityDimension {
37 Complexity,
39 Abstraction,
41 Deletion,
43 Hallucination,
45 Style,
47 Security,
49 Performance,
51 Duplication,
53 ErrorHandling,
55 Testing,
57 BestPractices,
59}
60
61impl QualityDimension {
62 pub fn all() -> &'static [QualityDimension] {
64 &[
65 QualityDimension::Complexity,
66 QualityDimension::Abstraction,
67 QualityDimension::Deletion,
68 QualityDimension::Hallucination,
69 QualityDimension::Style,
70 QualityDimension::Security,
71 QualityDimension::Performance,
72 QualityDimension::Duplication,
73 QualityDimension::ErrorHandling,
74 QualityDimension::Testing,
75 QualityDimension::BestPractices,
76 ]
77 }
78
79 pub fn display_name(&self) -> &'static str {
81 match self {
82 QualityDimension::Complexity => "Complexity",
83 QualityDimension::Abstraction => "Abstraction",
84 QualityDimension::Deletion => "Unintended Deletion",
85 QualityDimension::Hallucination => "Hallucinated Components",
86 QualityDimension::Style => "Style Inconsistencies",
87 QualityDimension::Security => "Security Vulnerabilities",
88 QualityDimension::Performance => "Performance Issues",
89 QualityDimension::Duplication => "Code Duplication",
90 QualityDimension::ErrorHandling => "Error Handling",
91 QualityDimension::Testing => "Test Coverage",
92 QualityDimension::BestPractices => "Best Practices",
93 }
94 }
95
96 #[allow(clippy::too_many_lines)]
98 pub fn description(&self) -> &'static str {
99 match self {
100 QualityDimension::Complexity => {
101 "
102 **Unnecessary Complexity**
103 - Overly complex algorithms or functions
104 - Unnecessary abstraction layers
105 - Convoluted control flow
106 - Functions/methods that are too long or have too many parameters
107 - Nesting levels that are too deep
108 "
109 }
110 QualityDimension::Abstraction => {
111 "
112 **Poor Abstractions**
113 - Inappropriate use of design patterns
114 - Missing abstractions where needed
115 - Leaky abstractions that expose implementation details
116 - Overly generic abstractions that add complexity
117 - Unclear separation of concerns
118 "
119 }
120 QualityDimension::Deletion => {
121 "
122 **Unintended Code Deletion**
123 - Critical functionality removed without replacement
124 - Incomplete removal of deprecated code
125 - Breaking changes to public APIs
126 - Removed error handling or validation
127 - Missing edge case handling present in original code
128 "
129 }
130 QualityDimension::Hallucination => {
131 "
132 **Hallucinated Components**
133 - References to non-existent functions, classes, or modules
134 - Assumptions about available libraries or APIs
135 - Inconsistent or impossible behavior expectations
136 - References to frameworks or patterns not used in the project
137 - Creation of interfaces that don't align with the codebase
138 "
139 }
140 QualityDimension::Style => {
141 "
142 **Style Inconsistencies**
143 - Deviation from project coding standards
144 - Inconsistent naming conventions
145 - Inconsistent formatting or indentation
146 - Inconsistent comment styles or documentation
147 - Mixing of different programming paradigms
148 "
149 }
150 QualityDimension::Security => {
151 "
152 **Security Vulnerabilities**
153 - Injection vulnerabilities (SQL, Command, etc.)
154 - Insecure data handling or storage
155 - Authentication or authorization flaws
156 - Exposure of sensitive information
157 - Unsafe dependencies or API usage
158 "
159 }
160 QualityDimension::Performance => {
161 "
162 **Performance Issues**
163 - Inefficient algorithms or data structures
164 - Unnecessary computations or operations
165 - Resource leaks (memory, file handles, etc.)
166 - Excessive network or disk operations
167 - Blocking operations in asynchronous code
168 "
169 }
170 QualityDimension::Duplication => {
171 "
172 **Code Duplication**
173 - Repeated logic or functionality
174 - Copy-pasted code with minor variations
175 - Duplicate functionality across different modules
176 - Redundant validation or error handling
177 - Parallel hierarchies or structures
178 "
179 }
180 QualityDimension::ErrorHandling => {
181 "
182 **Incomplete Error Handling**
183 - Missing try-catch blocks for risky operations
184 - Overly broad exception handling
185 - Swallowed exceptions without proper logging
186 - Unclear error messages or codes
187 - Inconsistent error recovery strategies
188 "
189 }
190 QualityDimension::Testing => {
191 "
192 **Test Coverage Gaps**
193 - Missing unit tests for critical functionality
194 - Uncovered edge cases or error paths
195 - Brittle tests that make inappropriate assumptions
196 - Missing integration or system tests
197 - Tests that don't verify actual requirements
198 "
199 }
200 QualityDimension::BestPractices => {
201 "
202 **Best Practices Violations**
203 - Not following language-specific idioms and conventions
204 - Violation of SOLID principles or other design guidelines
205 - Anti-patterns or known problematic implementation approaches
206 - Ignored compiler/linter warnings
207 - Outdated or deprecated APIs and practices
208 "
209 }
210 }
211 }
212}
213
214#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
216pub struct GeneratedReview {
217 pub summary: String,
219 pub code_quality: String,
221 pub suggestions: Vec<String>,
223 pub issues: Vec<String>,
225 pub positive_aspects: Vec<String>,
227 pub complexity: Option<DimensionAnalysis>,
229 pub abstraction: Option<DimensionAnalysis>,
231 pub deletion: Option<DimensionAnalysis>,
233 pub hallucination: Option<DimensionAnalysis>,
235 pub style: Option<DimensionAnalysis>,
237 pub security: Option<DimensionAnalysis>,
239 pub performance: Option<DimensionAnalysis>,
241 pub duplication: Option<DimensionAnalysis>,
243 pub error_handling: Option<DimensionAnalysis>,
245 pub testing: Option<DimensionAnalysis>,
247 pub best_practices: Option<DimensionAnalysis>,
249}
250
251impl GeneratedReview {
252 pub fn format_location(location: &str) -> String {
257 if location.contains(':')
258 || location.to_lowercase().contains(".rs")
259 || location.to_lowercase().contains(".ts")
260 || location.to_lowercase().contains(".js")
261 || location.to_lowercase().contains("file")
262 {
263 location.to_string()
265 } else if location.to_lowercase().contains("line") {
266 location.to_string()
268 } else {
269 format!("Line(s) {location}")
271 }
272 }
273
274 pub fn format(&self) -> String {
276 let mut formatted = String::new();
277
278 formatted.push_str(&format!(
279 "{}\n\n{}\n\n",
280 "✧・゚: *✧・゚ CODE REVIEW ✧・゚: *✧・゚".bright_magenta().bold(),
281 self.summary.bright_white()
282 ));
283
284 formatted.push_str(&format!(
285 "{}\n\n{}\n\n",
286 "◤ QUALITY ASSESSMENT ◢".bright_cyan().bold(),
287 self.code_quality.bright_white()
288 ));
289
290 if !self.positive_aspects.is_empty() {
291 formatted.push_str(&format!("{}\n\n", "✅ STRENGTHS //".green().bold()));
292 for aspect in &self.positive_aspects {
293 formatted.push_str(&format!(" {} {}\n", "•".bright_green(), aspect.green()));
294 }
295 formatted.push('\n');
296 }
297
298 if !self.issues.is_empty() {
299 formatted.push_str(&format!("{}\n\n", "⚠️ CORE ISSUES //".yellow().bold()));
300 for issue in &self.issues {
301 formatted.push_str(&format!(" {} {}\n", "•".bright_yellow(), issue.yellow()));
302 }
303 formatted.push('\n');
304 }
305
306 Self::format_dimension_analysis(
308 &mut formatted,
309 QualityDimension::Complexity,
310 self.complexity.as_ref(),
311 );
312 Self::format_dimension_analysis(
313 &mut formatted,
314 QualityDimension::Abstraction,
315 self.abstraction.as_ref(),
316 );
317 Self::format_dimension_analysis(
318 &mut formatted,
319 QualityDimension::Deletion,
320 self.deletion.as_ref(),
321 );
322 Self::format_dimension_analysis(
323 &mut formatted,
324 QualityDimension::Hallucination,
325 self.hallucination.as_ref(),
326 );
327 Self::format_dimension_analysis(
328 &mut formatted,
329 QualityDimension::Style,
330 self.style.as_ref(),
331 );
332 Self::format_dimension_analysis(
333 &mut formatted,
334 QualityDimension::Security,
335 self.security.as_ref(),
336 );
337 Self::format_dimension_analysis(
338 &mut formatted,
339 QualityDimension::Performance,
340 self.performance.as_ref(),
341 );
342 Self::format_dimension_analysis(
343 &mut formatted,
344 QualityDimension::Duplication,
345 self.duplication.as_ref(),
346 );
347 Self::format_dimension_analysis(
348 &mut formatted,
349 QualityDimension::ErrorHandling,
350 self.error_handling.as_ref(),
351 );
352 Self::format_dimension_analysis(
353 &mut formatted,
354 QualityDimension::Testing,
355 self.testing.as_ref(),
356 );
357 Self::format_dimension_analysis(
358 &mut formatted,
359 QualityDimension::BestPractices,
360 self.best_practices.as_ref(),
361 );
362
363 if !self.suggestions.is_empty() {
364 formatted.push_str(&format!("{}\n\n", "💡 SUGGESTIONS //".bright_blue().bold()));
365 for suggestion in &self.suggestions {
366 formatted.push_str(&format!(
367 " {} {}\n",
368 "•".bright_cyan(),
369 suggestion.bright_blue()
370 ));
371 }
372 }
373
374 formatted
375 }
376
377 fn format_dimension_analysis(
379 formatted: &mut String,
380 dimension: QualityDimension,
381 analysis: Option<&DimensionAnalysis>,
382 ) {
383 if let Some(dim) = analysis {
384 if dim.issues_found && !dim.issues.is_empty() {
385 let (emoji, color_fn) = match dimension {
387 QualityDimension::Complexity => ("🧠", "bright_magenta"),
388 QualityDimension::Abstraction => ("🏗️", "bright_cyan"),
389 QualityDimension::Deletion => ("🗑️", "bright_white"),
390 QualityDimension::Hallucination => ("👻", "bright_magenta"),
391 QualityDimension::Style => ("🎨", "bright_blue"),
392 QualityDimension::Security => ("🔒", "bright_red"),
393 QualityDimension::Performance => ("⚡", "bright_yellow"),
394 QualityDimension::Duplication => ("🔄", "bright_cyan"),
395 QualityDimension::ErrorHandling => ("🧯", "bright_red"),
396 QualityDimension::Testing => ("🧪", "bright_green"),
397 QualityDimension::BestPractices => ("📐", "bright_blue"),
398 };
399
400 let title = dimension.display_name();
401 let header = match color_fn {
402 "bright_magenta" => format!("◤ {emoji} {title} ◢").bright_magenta().bold(),
403 "bright_cyan" => format!("◤ {emoji} {title} ◢").bright_cyan().bold(),
404 "bright_white" => format!("◤ {emoji} {title} ◢").bright_white().bold(),
405 "bright_blue" => format!("◤ {emoji} {title} ◢").bright_blue().bold(),
406 "bright_red" => format!("◤ {emoji} {title} ◢").bright_red().bold(),
407 "bright_yellow" => format!("◤ {emoji} {title} ◢").bright_yellow().bold(),
408 "bright_green" => format!("◤ {emoji} {title} ◢").bright_green().bold(),
409 _ => format!("◤ {emoji} {title} ◢").normal().bold(),
410 };
411
412 formatted.push_str(&format!("{header}\n\n"));
413
414 for (i, issue) in dim.issues.iter().enumerate() {
415 let severity_badge = match issue.severity.as_str() {
417 "Critical" => format!("[{}]", "CRITICAL".bright_red().bold()),
418 "High" => format!("[{}]", "HIGH".red().bold()),
419 "Medium" => format!("[{}]", "MEDIUM".yellow().bold()),
420 "Low" => format!("[{}]", "LOW".bright_yellow().bold()),
421 _ => format!("[{}]", issue.severity.normal().bold()),
422 };
423
424 formatted.push_str(&format!(
425 " {} {} {}\n",
426 format!("{:02}", i + 1).bright_white().bold(),
427 severity_badge,
428 issue.description.bright_white()
429 ));
430
431 let formatted_location = Self::format_location(&issue.location).bright_white();
432 formatted.push_str(&format!(
433 " {}: {}\n",
434 "LOCATION".bright_cyan().bold(),
435 formatted_location
436 ));
437
438 let explanation_lines =
440 textwrap::wrap(&issue.explanation, EXPLANATION_WRAP_WIDTH);
441 formatted.push_str(&format!(" {}: ", "DETAIL".bright_cyan().bold()));
442 for (i, line) in explanation_lines.iter().enumerate() {
443 if i == 0 {
444 formatted.push_str(&format!("{line}\n"));
445 } else {
446 formatted.push_str(&format!(" {line}\n"));
447 }
448 }
449
450 formatted.push_str(&format!(
452 " {}: {}\n\n",
453 "FIX".bright_green().bold(),
454 issue.recommendation.bright_green()
455 ));
456 }
457 }
458 }
459 }
460}
461
462use super::service::IrisCommitService;
463use crate::common::CommonParams;
464use crate::config::Config;
465use crate::git::GitRepo;
466use crate::instruction_presets::PresetType;
467use crate::messages;
468use crate::ui;
469use anyhow::{Context, Result};
470use std::sync::Arc;
471
472pub async fn handle_review_command(
475 common: CommonParams,
476 _print: bool,
477 repository_url: Option<String>,
478 include_unstaged: bool,
479 commit_id: Option<String>,
480) -> Result<()> {
481 if !common.is_valid_preset_for_type(PresetType::Review) {
483 ui::print_warning(
484 "The specified preset may not be suitable for code reviews. Consider using a review or general preset instead.",
485 );
486 ui::print_info("Run 'git-iris list-presets' to see available presets for reviews.");
487 }
488
489 let mut config = Config::load()?;
490 common.apply_to_config(&mut config)?;
491
492 let repo_url = repository_url.or(common.repository_url.clone());
494
495 let git_repo = GitRepo::new_from_url(repo_url).context("Failed to create GitRepo")?;
497
498 let repo_path = git_repo.repo_path().clone();
499 let provider_name = &config.default_provider;
500
501 let service = Arc::new(
502 IrisCommitService::new(
503 config.clone(),
504 &repo_path,
505 provider_name,
506 false, false, git_repo,
509 )
510 .context("Failed to create IrisCommitService")?,
511 );
512
513 if let Err(e) = service.check_environment() {
515 ui::print_error(&format!("Error: {e}"));
516 ui::print_info("\nPlease ensure the following:");
517 ui::print_info("1. Git is installed and accessible from the command line.");
518 ui::print_info(
519 "2. You are running this command from within a Git repository or provide a repository URL with --repo.",
520 );
521 ui::print_info("3. You have set up your configuration using 'git-iris config'.");
522 return Err(e);
523 }
524
525 let effective_instructions = common
526 .instructions
527 .unwrap_or_else(|| config.instructions.clone());
528 let preset_str = common.preset.as_deref().unwrap_or("");
529
530 let spinner = ui::create_spinner("");
532 let random_message = messages::get_review_waiting_message();
533 spinner.set_message(random_message.text.to_string());
534
535 let review = if let Some(commit_id) = commit_id {
537 spinner.set_message(format!(
539 "{} - Reviewing commit: {}",
540 random_message.text, commit_id
541 ));
542
543 service
545 .generate_review_for_commit(preset_str, &effective_instructions, &commit_id)
546 .await?
547 } else {
548 if include_unstaged {
550 spinner.set_message(format!(
551 "{} - Including unstaged changes",
552 random_message.text
553 ));
554
555 let git_info = service.get_git_info_with_unstaged(include_unstaged).await?;
557
558 if git_info.staged_files.is_empty() {
559 spinner.finish_and_clear();
560 ui::print_warning("No changes found (staged or unstaged). Nothing to review.");
561 return Ok(());
562 }
563
564 service
566 .generate_review_with_unstaged(
567 preset_str,
568 &effective_instructions,
569 include_unstaged,
570 )
571 .await?
572 } else {
573 let git_info = service.get_git_info().await?;
575
576 if git_info.staged_files.is_empty() {
577 spinner.finish_and_clear();
578 ui::print_warning(
579 "No staged changes. Please stage your changes before generating a review.",
580 );
581 ui::print_info("You can stage changes using 'git add <file>' or 'git add .'");
582 ui::print_info("To include unstaged changes, use --include-unstaged");
583 return Ok(());
584 }
585
586 service
588 .generate_review(preset_str, &effective_instructions)
589 .await?
590 }
591 };
592
593 spinner.finish_and_clear();
595
596 println!("{}", review.format());
598
599 Ok(())
600}