git_iris/commit/
review.rs

1use colored::Colorize;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use textwrap;
5
6/// Width in characters for wrapping explanations in code reviews
7const EXPLANATION_WRAP_WIDTH: usize = 80;
8
9/// Represents a specific issue found during code review
10#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
11pub struct CodeIssue {
12    /// Brief description of the issue
13    pub description: String,
14    /// Severity level of the issue (Critical, High, Medium, Low)
15    pub severity: String,
16    /// Location of the issue, preferably in "`filename.rs:line_numbers`" format
17    /// or "`path/to/file.rs:line_numbers`" format for better readability
18    pub location: String,
19    /// Detailed explanation of why this is problematic
20    pub explanation: String,
21    /// Specific suggestion to address the issue
22    pub recommendation: String,
23}
24
25/// Represents analysis for a specific code quality dimension
26#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
27pub struct DimensionAnalysis {
28    /// Whether issues were found in this dimension
29    pub issues_found: bool,
30    /// List of specific issues identified in this dimension
31    pub issues: Vec<CodeIssue>,
32}
33
34/// Represents the different dimensions of code quality analysis
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
36pub enum QualityDimension {
37    /// Unnecessary complexity in algorithms, abstractions, or control flow
38    Complexity,
39    /// Poor or inappropriate abstractions, design patterns or separation of concerns
40    Abstraction,
41    /// Unintended deletion of code or functionality without proper replacement
42    Deletion,
43    /// References to non-existent components, APIs, or behaviors
44    Hallucination,
45    /// Inconsistencies in code style, naming, or formatting
46    Style,
47    /// Security vulnerabilities or insecure coding practices
48    Security,
49    /// Inefficient algorithms, operations, or resource usage
50    Performance,
51    /// Repeated logic, functionality, or copy-pasted code
52    Duplication,
53    /// Insufficient or improper error handling and recovery
54    ErrorHandling,
55    /// Gaps in test coverage or tests that miss critical scenarios
56    Testing,
57    /// Violations of established best practices or coding standards
58    BestPractices,
59}
60
61impl QualityDimension {
62    /// Get all quality dimensions
63    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    /// Get the display name for a dimension
80    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    /// Get the description for a dimension
97    #[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/// Model for code review generation results
215#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
216pub struct GeneratedReview {
217    /// Brief summary of the code changes and overall review
218    pub summary: String,
219    /// Detailed assessment of the overall code quality
220    pub code_quality: String,
221    /// List of specific suggestions for improving the code
222    pub suggestions: Vec<String>,
223    /// List of identified issues or problems in the code
224    pub issues: Vec<String>,
225    /// List of positive aspects or good practices in the code
226    pub positive_aspects: Vec<String>,
227    /// Analysis of unnecessary complexity issues
228    pub complexity: Option<DimensionAnalysis>,
229    /// Analysis of abstraction quality issues
230    pub abstraction: Option<DimensionAnalysis>,
231    /// Analysis of unintended code deletion
232    pub deletion: Option<DimensionAnalysis>,
233    /// Analysis of hallucinated components that don't exist
234    pub hallucination: Option<DimensionAnalysis>,
235    /// Analysis of style inconsistencies
236    pub style: Option<DimensionAnalysis>,
237    /// Analysis of security vulnerabilities
238    pub security: Option<DimensionAnalysis>,
239    /// Analysis of performance issues
240    pub performance: Option<DimensionAnalysis>,
241    /// Analysis of code duplication
242    pub duplication: Option<DimensionAnalysis>,
243    /// Analysis of error handling completeness
244    pub error_handling: Option<DimensionAnalysis>,
245    /// Analysis of test coverage gaps
246    pub testing: Option<DimensionAnalysis>,
247    /// Analysis of best practices violations
248    pub best_practices: Option<DimensionAnalysis>,
249}
250
251impl GeneratedReview {
252    /// Formats a location string to ensure it includes file reference when possible
253    ///
254    /// Intelligently formats location strings by detecting whether they already
255    /// contain a file reference or just line numbers.
256    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            // This is likely a file reference
264            location.to_string()
265        } else if location.to_lowercase().contains("line") {
266            // This already mentions line numbers
267            location.to_string()
268        } else {
269            // Treat as just line numbers - explicitly mention it's line numbers
270            format!("Line(s) {location}")
271        }
272    }
273
274    /// Formats the review into a readable string with colors and emojis for terminal display
275    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        // Format the dimension-specific analyses if they exist
307        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    /// Helper method to format a single dimension analysis
378    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                // Choose emoji based on the dimension
386                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                    // Severity badge with custom styling based on severity
416                    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                    // Format explanation with some spacing for readability
439                    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                    // Format recommendation with a different style
451                    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
472/// Handles the review command which generates an AI code review of staged changes
473/// with comprehensive analysis across multiple dimensions of code quality
474pub 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    // Check if the preset is appropriate for code reviews
482    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    // Combine repository URL from CLI and CommonParams
493    let repo_url = repository_url.or(common.repository_url.clone());
494
495    // Create the git repository
496    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, // gitmoji not needed for review
507            false, // verification not needed for review
508            git_repo,
509        )
510        .context("Failed to create IrisCommitService")?,
511    );
512
513    // Check environment prerequisites
514    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    // Create and start the spinner
531    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    // Generate the code review, with different approaches based on parameters
536    let review = if let Some(commit_id) = commit_id {
537        // Show the commit we're reviewing
538        spinner.set_message(format!(
539            "{} - Reviewing commit: {}",
540            random_message.text, commit_id
541        ));
542
543        // Get the review for the specified commit
544        service
545            .generate_review_for_commit(preset_str, &effective_instructions, &commit_id)
546            .await?
547    } else {
548        // Check if we should include unstaged changes
549        if include_unstaged {
550            spinner.set_message(format!(
551                "{} - Including unstaged changes",
552                random_message.text
553            ));
554
555            // Get the git info with unstaged changes to check if there are any changes
556            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            // Generate the review with unstaged changes
565            service
566                .generate_review_with_unstaged(
567                    preset_str,
568                    &effective_instructions,
569                    include_unstaged,
570                )
571                .await?
572        } else {
573            // Original behavior - only staged changes
574            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            // Generate the review with only staged changes
587            service
588                .generate_review(preset_str, &effective_instructions)
589                .await?
590        }
591    };
592
593    // Stop the spinner
594    spinner.finish_and_clear();
595
596    // Print the review to stdout or save to file if requested
597    println!("{}", review.format());
598
599    Ok(())
600}