1use super::service::CommitService;
2use crate::common::CommonParams;
3use crate::config::Config;
4use crate::core::messages;
5use crate::git::GitRepo;
6use crate::instruction_presets::PresetType;
7use crate::ui;
8
9use anyhow::{Context, Result};
10use colored::Colorize;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use std::fmt::Write;
14use std::sync::Arc;
15use textwrap;
16
17const EXPLANATION_WRAP_WIDTH: usize = 80;
19
20#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
22pub struct CodeIssue {
23 pub description: String,
25 pub severity: String,
27 pub location: String,
30 pub explanation: String,
32 pub recommendation: String,
34}
35
36#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
38pub struct DimensionAnalysis {
39 pub issues_found: bool,
41 pub issues: Vec<CodeIssue>,
43}
44
45#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
47pub enum QualityDimension {
48 Complexity,
50 Abstraction,
52 Deletion,
54 Hallucination,
56 Style,
58 Security,
60 Performance,
62 Duplication,
64 ErrorHandling,
66 Testing,
68 BestPractices,
70}
71
72impl QualityDimension {
73 pub fn all() -> &'static [QualityDimension] {
75 &[
76 QualityDimension::Complexity,
77 QualityDimension::Abstraction,
78 QualityDimension::Deletion,
79 QualityDimension::Hallucination,
80 QualityDimension::Style,
81 QualityDimension::Security,
82 QualityDimension::Performance,
83 QualityDimension::Duplication,
84 QualityDimension::ErrorHandling,
85 QualityDimension::Testing,
86 QualityDimension::BestPractices,
87 ]
88 }
89
90 pub fn display_name(&self) -> &'static str {
92 match self {
93 QualityDimension::Complexity => "Complexity",
94 QualityDimension::Abstraction => "Abstraction",
95 QualityDimension::Deletion => "Unintended Deletion",
96 QualityDimension::Hallucination => "Hallucinated Components",
97 QualityDimension::Style => "Style Inconsistencies",
98 QualityDimension::Security => "Security Vulnerabilities",
99 QualityDimension::Performance => "Performance Issues",
100 QualityDimension::Duplication => "Code Duplication",
101 QualityDimension::ErrorHandling => "Error Handling",
102 QualityDimension::Testing => "Test Coverage",
103 QualityDimension::BestPractices => "Best Practices",
104 }
105 }
106
107 #[allow(clippy::too_many_lines)]
109 pub fn description(&self) -> &'static str {
110 match self {
111 QualityDimension::Complexity => {
112 "
113 **Unnecessary Complexity**
114 - Overly complex algorithms or functions
115 - Unnecessary abstraction layers
116 - Convoluted control flow
117 - Functions/methods that are too long or have too many parameters
118 - Nesting levels that are too deep
119 "
120 }
121 QualityDimension::Abstraction => {
122 "
123 **Poor Abstractions**
124 - Inappropriate use of design patterns
125 - Missing abstractions where needed
126 - Leaky abstractions that expose implementation details
127 - Overly generic abstractions that add complexity
128 - Unclear separation of concerns
129 "
130 }
131 QualityDimension::Deletion => {
132 "
133 **Unintended Code Deletion**
134 - Critical functionality removed without replacement
135 - Incomplete removal of deprecated code
136 - Breaking changes to public APIs
137 - Removed error handling or validation
138 - Missing edge case handling present in original code
139 "
140 }
141 QualityDimension::Hallucination => {
142 "
143 **Hallucinated Components**
144 - References to non-existent functions, classes, or modules
145 - Assumptions about available libraries or APIs
146 - Inconsistent or impossible behavior expectations
147 - References to frameworks or patterns not used in the project
148 - Creation of interfaces that don't align with the codebase
149 "
150 }
151 QualityDimension::Style => {
152 "
153 **Style Inconsistencies**
154 - Deviation from project coding standards
155 - Inconsistent naming conventions
156 - Inconsistent formatting or indentation
157 - Inconsistent comment styles or documentation
158 - Mixing of different programming paradigms
159 "
160 }
161 QualityDimension::Security => {
162 "
163 **Security Vulnerabilities**
164 - Injection vulnerabilities (SQL, Command, etc.)
165 - Insecure data handling or storage
166 - Authentication or authorization flaws
167 - Exposure of sensitive information
168 - Unsafe dependencies or API usage
169 "
170 }
171 QualityDimension::Performance => {
172 "
173 **Performance Issues**
174 - Inefficient algorithms or data structures
175 - Unnecessary computations or operations
176 - Resource leaks (memory, file handles, etc.)
177 - Excessive network or disk operations
178 - Blocking operations in asynchronous code
179 "
180 }
181 QualityDimension::Duplication => {
182 "
183 **Code Duplication**
184 - Repeated logic or functionality
185 - Copy-pasted code with minor variations
186 - Duplicate functionality across different modules
187 - Redundant validation or error handling
188 - Parallel hierarchies or structures
189 "
190 }
191 QualityDimension::ErrorHandling => {
192 "
193 **Incomplete Error Handling**
194 - Missing try-catch blocks for risky operations
195 - Overly broad exception handling
196 - Swallowed exceptions without proper logging
197 - Unclear error messages or codes
198 - Inconsistent error recovery strategies
199 "
200 }
201 QualityDimension::Testing => {
202 "
203 **Test Coverage Gaps**
204 - Missing unit tests for critical functionality
205 - Uncovered edge cases or error paths
206 - Brittle tests that make inappropriate assumptions
207 - Missing integration or system tests
208 - Tests that don't verify actual requirements
209 "
210 }
211 QualityDimension::BestPractices => {
212 "
213 **Best Practices Violations**
214 - Not following language-specific idioms and conventions
215 - Violation of SOLID principles or other design guidelines
216 - Anti-patterns or known problematic implementation approaches
217 - Ignored compiler/linter warnings
218 - Outdated or deprecated APIs and practices
219 "
220 }
221 }
222 }
223}
224
225#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
227pub struct GeneratedReview {
228 pub summary: String,
230 pub code_quality: String,
232 pub suggestions: Vec<String>,
234 pub issues: Vec<String>,
236 pub positive_aspects: Vec<String>,
238 pub complexity: Option<DimensionAnalysis>,
240 pub abstraction: Option<DimensionAnalysis>,
242 pub deletion: Option<DimensionAnalysis>,
244 pub hallucination: Option<DimensionAnalysis>,
246 pub style: Option<DimensionAnalysis>,
248 pub security: Option<DimensionAnalysis>,
250 pub performance: Option<DimensionAnalysis>,
252 pub duplication: Option<DimensionAnalysis>,
254 pub error_handling: Option<DimensionAnalysis>,
256 pub testing: Option<DimensionAnalysis>,
258 pub best_practices: Option<DimensionAnalysis>,
260}
261
262impl GeneratedReview {
263 pub fn format_location(location: &str) -> String {
265 if location.to_lowercase().contains("line")
267 || location.to_lowercase().contains("file")
268 || location.to_lowercase().contains(" in ")
269 {
270 return location.to_string();
271 }
272
273 if location.contains(':') && (location.contains('/') || location.contains('\\')) {
275 location.to_string()
276 } else if location.contains(':') {
277 format!("in {location}")
279 } else if location.contains('.')
280 && location
281 .split('.')
282 .next_back()
283 .is_some_and(|ext| !ext.is_empty())
284 {
285 location.to_string()
287 } else {
288 format!("Line(s) {location}")
290 }
291 }
292
293 pub fn format(&self) -> String {
295 let mut formatted = String::new();
296
297 Self::format_header(&mut formatted, &self.summary, &self.code_quality);
298 Self::format_positive_aspects(&mut formatted, &self.positive_aspects);
299 Self::format_issues(&mut formatted, &self.issues);
300 Self::format_all_dimension_analyses(&mut formatted, self);
301 Self::format_suggestions(&mut formatted, &self.suggestions);
302
303 formatted
304 }
305
306 fn format_header(formatted: &mut String, summary: &str, code_quality: &str) {
308 write!(
309 formatted,
310 "{}\n\n{}\n\n",
311 "✧・゚: *✧・゚ CODE REVIEW ✧・゚: *✧・゚".bright_magenta().bold(),
312 summary.bright_white()
313 )
314 .expect("write to string should not fail");
315
316 write!(
317 formatted,
318 "{}\n\n{}\n\n",
319 "◤ QUALITY ASSESSMENT ◢".bright_cyan().bold(),
320 code_quality.bright_white()
321 )
322 .expect("write to string should not fail");
323 }
324
325 fn format_positive_aspects(formatted: &mut String, positive_aspects: &[String]) {
327 if !positive_aspects.is_empty() {
328 write!(formatted, "{}\n\n", "✅ STRENGTHS //".green().bold())
329 .expect("write to string should not fail");
330 for aspect in positive_aspects {
331 writeln!(formatted, " {} {}", "•".bright_green(), aspect.green())
332 .expect("write to string should not fail");
333 }
334 formatted.push('\n');
335 }
336 }
337
338 fn format_issues(formatted: &mut String, issues: &[String]) {
340 if !issues.is_empty() {
341 write!(formatted, "{}\n\n", "⚠️ CORE ISSUES //".yellow().bold())
342 .expect("write to string should not fail");
343 for issue in issues {
344 writeln!(formatted, " {} {}", "•".bright_yellow(), issue.yellow())
345 .expect("write to string should not fail");
346 }
347 formatted.push('\n');
348 }
349 }
350
351 fn format_all_dimension_analyses(formatted: &mut String, review: &GeneratedReview) {
353 Self::format_dimension_analysis(
354 formatted,
355 QualityDimension::Complexity,
356 review.complexity.as_ref(),
357 );
358 Self::format_dimension_analysis(
359 formatted,
360 QualityDimension::Abstraction,
361 review.abstraction.as_ref(),
362 );
363 Self::format_dimension_analysis(
364 formatted,
365 QualityDimension::Deletion,
366 review.deletion.as_ref(),
367 );
368 Self::format_dimension_analysis(
369 formatted,
370 QualityDimension::Hallucination,
371 review.hallucination.as_ref(),
372 );
373 Self::format_dimension_analysis(formatted, QualityDimension::Style, review.style.as_ref());
374 Self::format_dimension_analysis(
375 formatted,
376 QualityDimension::Security,
377 review.security.as_ref(),
378 );
379 Self::format_dimension_analysis(
380 formatted,
381 QualityDimension::Performance,
382 review.performance.as_ref(),
383 );
384 Self::format_dimension_analysis(
385 formatted,
386 QualityDimension::Duplication,
387 review.duplication.as_ref(),
388 );
389 Self::format_dimension_analysis(
390 formatted,
391 QualityDimension::ErrorHandling,
392 review.error_handling.as_ref(),
393 );
394 Self::format_dimension_analysis(
395 formatted,
396 QualityDimension::Testing,
397 review.testing.as_ref(),
398 );
399 Self::format_dimension_analysis(
400 formatted,
401 QualityDimension::BestPractices,
402 review.best_practices.as_ref(),
403 );
404 }
405
406 fn format_suggestions(formatted: &mut String, suggestions: &[String]) {
408 if !suggestions.is_empty() {
409 write!(
410 formatted,
411 "{}\n\n",
412 "💡 SUGGESTIONS //".bright_blue().bold()
413 )
414 .expect("write to string should not fail");
415 for suggestion in suggestions {
416 writeln!(
417 formatted,
418 " {} {}",
419 "•".bright_cyan(),
420 suggestion.bright_blue()
421 )
422 .expect("write to string should not fail");
423 }
424 }
425 }
426
427 fn format_dimension_analysis(
429 formatted: &mut String,
430 dimension: QualityDimension,
431 analysis: Option<&DimensionAnalysis>,
432 ) {
433 if let Some(dim) = analysis
434 && dim.issues_found
435 && !dim.issues.is_empty()
436 {
437 let (emoji, color_fn) = match dimension {
439 QualityDimension::Complexity => ("🧠", "bright_magenta"),
440 QualityDimension::Abstraction => ("🏗️", "bright_cyan"),
441 QualityDimension::Deletion => ("🗑️", "bright_white"),
442 QualityDimension::Hallucination => ("👻", "bright_magenta"),
443 QualityDimension::Style => ("🎨", "bright_blue"),
444 QualityDimension::Security => ("🔒", "bright_red"),
445 QualityDimension::Performance => ("⚡", "bright_yellow"),
446 QualityDimension::Duplication => ("🔄", "bright_cyan"),
447 QualityDimension::ErrorHandling => ("🧯", "bright_red"),
448 QualityDimension::Testing => ("🧪", "bright_green"),
449 QualityDimension::BestPractices => ("📐", "bright_blue"),
450 };
451
452 let title = dimension.display_name();
453 let header = match color_fn {
454 "bright_magenta" => format!("◤ {emoji} {title} ◢").bright_magenta().bold(),
455 "bright_cyan" => format!("◤ {emoji} {title} ◢").bright_cyan().bold(),
456 "bright_white" => format!("◤ {emoji} {title} ◢").bright_white().bold(),
457 "bright_blue" => format!("◤ {emoji} {title} ◢").bright_blue().bold(),
458 "bright_red" => format!("◤ {emoji} {title} ◢").bright_red().bold(),
459 "bright_yellow" => format!("◤ {emoji} {title} ◢").bright_yellow().bold(),
460 "bright_green" => format!("◤ {emoji} {title} ◢").bright_green().bold(),
461 _ => format!("◤ {emoji} {title} ◢").normal().bold(),
462 };
463
464 write!(formatted, "{header}\n\n").expect("write to string should not fail");
465
466 for (i, issue) in dim.issues.iter().enumerate() {
467 let severity_badge = match issue.severity.as_str() {
469 "Critical" => format!("[{}]", "CRITICAL".bright_red().bold()),
470 "High" => format!("[{}]", "HIGH".red().bold()),
471 "Medium" => format!("[{}]", "MEDIUM".yellow().bold()),
472 "Low" => format!("[{}]", "LOW".bright_yellow().bold()),
473 _ => format!("[{}]", issue.severity.normal().bold()),
474 };
475
476 writeln!(
477 formatted,
478 " {} {} {}",
479 format!("{:02}", i + 1).bright_white().bold(),
480 severity_badge,
481 issue.description.bright_white()
482 )
483 .expect("write to string should not fail");
484
485 let formatted_location = Self::format_location(&issue.location).bright_white();
486 writeln!(
487 formatted,
488 " {}: {}",
489 "LOCATION".bright_cyan().bold(),
490 formatted_location
491 )
492 .expect("write to string should not fail");
493
494 let explanation_lines = textwrap::wrap(&issue.explanation, EXPLANATION_WRAP_WIDTH);
496 write!(formatted, " {}: ", "DETAIL".bright_cyan().bold())
497 .expect("write to string should not fail");
498 for (i, line) in explanation_lines.iter().enumerate() {
499 if i == 0 {
500 writeln!(formatted, "{line}").expect("write to string should not fail");
501 } else {
502 writeln!(formatted, " {line}")
503 .expect("write to string should not fail");
504 }
505 }
506
507 write!(
509 formatted,
510 " {}: {}\n\n",
511 "FIX".bright_green().bold(),
512 issue.recommendation.bright_green()
513 )
514 .expect("write to string should not fail");
515 }
516 }
517 }
518}
519
520pub async fn handle_review_command(
523 common: CommonParams,
524 _print: bool,
525 repository_url: Option<String>,
526 include_unstaged: bool,
527 commit_id: Option<String>,
528 from: Option<String>,
529 to: Option<String>,
530) -> Result<()> {
531 validate_preset_for_review(&common);
533
534 validate_review_parameters(
536 commit_id.as_ref(),
537 from.as_ref(),
538 to.as_ref(),
539 include_unstaged,
540 )?;
541
542 let mut config = Config::load()?;
543 common.apply_to_config(&mut config)?;
544
545 let service = setup_review_service(&common, repository_url, &config)?;
547
548 let review = generate_review_based_on_parameters(
550 service,
551 common,
552 config,
553 include_unstaged,
554 commit_id,
555 from,
556 to,
557 )
558 .await?;
559
560 println!("{}", review.format());
562
563 Ok(())
564}
565
566fn validate_preset_for_review(common: &CommonParams) {
568 if !common.is_valid_preset_for_type(PresetType::Review) {
569 ui::print_warning(
570 "The specified preset may not be suitable for code reviews. Consider using a review or general preset instead.",
571 );
572 ui::print_info("Run 'git presets' to see available presets for reviews.");
573 }
574}
575
576fn validate_review_parameters(
578 commit_id: Option<&String>,
579 from: Option<&String>,
580 to: Option<&String>,
581 include_unstaged: bool,
582) -> Result<()> {
583 if from.is_some() && to.is_none() {
584 return Err(anyhow::anyhow!(
585 "When using --from, you must also specify --to for branch comparison reviews"
586 ));
587 }
588
589 if commit_id.is_some() && (from.is_some() || to.is_some()) {
590 return Err(anyhow::anyhow!(
591 "Cannot use --commit with --from/--to. These are mutually exclusive options"
592 ));
593 }
594
595 if include_unstaged && (from.is_some() || to.is_some()) {
596 return Err(anyhow::anyhow!(
597 "Cannot use --include-unstaged with --from/--to. Branch comparison reviews don't include working directory changes"
598 ));
599 }
600
601 Ok(())
602}
603
604fn setup_review_service(
606 common: &CommonParams,
607 repository_url: Option<String>,
608 config: &Config,
609) -> Result<Arc<CommitService>> {
610 let repo_url = repository_url.or(common.repository_url.clone());
612
613 let git_repo = GitRepo::new_from_url(repo_url).context("Failed to create GitRepo")?;
615
616 let repo_path = git_repo.repo_path().clone();
617 let provider_name = &config.default_provider;
618
619 let service = Arc::new(
620 CommitService::new(
621 config.clone(),
622 &repo_path,
623 provider_name,
624 false, false, git_repo,
627 )
628 .context("Failed to create CommitService")?,
629 );
630
631 if let Err(e) = service.check_environment() {
633 ui::print_error(&format!("Error: {e}"));
634 ui::print_info("\nPlease ensure the following:");
635 ui::print_info("1. Git is installed and accessible from the command line.");
636 ui::print_info(
637 "2. You are running this command from within a Git repository or provide a repository URL with --repo.",
638 );
639 return Err(e);
640 }
641
642 Ok(service)
643}
644
645async fn generate_review_based_on_parameters(
647 service: Arc<CommitService>,
648 common: CommonParams,
649 config: Config,
650 include_unstaged: bool,
651 commit_id: Option<String>,
652 from: Option<String>,
653 to: Option<String>,
654) -> Result<GeneratedReview> {
655 let effective_instructions = common
656 .instructions
657 .unwrap_or_else(|| config.instructions.clone());
658 let preset_str = common.preset.as_deref().unwrap_or("");
659
660 let spinner = ui::create_spinner("");
662 let random_message = messages::get_review_waiting_message();
663 spinner.set_message(random_message.text.to_string());
664
665 let review = if let (Some(from_branch), Some(to_branch)) = (from.as_ref(), to.as_ref()) {
666 generate_branch_comparison_review(
668 &service,
669 &spinner,
670 random_message,
671 preset_str,
672 &effective_instructions,
673 from_branch,
674 to_branch,
675 )
676 .await?
677 } else if let Some(to_branch) = to.as_ref() {
678 let from_branch = "main";
680 generate_branch_comparison_review(
681 &service,
682 &spinner,
683 random_message,
684 preset_str,
685 &effective_instructions,
686 from_branch,
687 to_branch,
688 )
689 .await?
690 } else if let Some(commit_id) = commit_id {
691 generate_commit_review(
693 &service,
694 &spinner,
695 random_message,
696 preset_str,
697 &effective_instructions,
698 &commit_id,
699 )
700 .await?
701 } else {
702 generate_working_directory_review(
704 &service,
705 &spinner,
706 random_message,
707 preset_str,
708 &effective_instructions,
709 include_unstaged,
710 )
711 .await?
712 };
713
714 spinner.finish_and_clear();
716
717 Ok(review)
718}
719
720async fn generate_branch_comparison_review(
722 service: &Arc<CommitService>,
723 spinner: &indicatif::ProgressBar,
724 random_message: &messages::ColoredMessage,
725 preset_str: &str,
726 effective_instructions: &str,
727 from_branch: &str,
728 to_branch: &str,
729) -> Result<GeneratedReview> {
730 spinner.set_message(format!(
731 "{} - Comparing {} -> {}",
732 random_message.text, from_branch, to_branch
733 ));
734
735 service
736 .generate_review_for_branch_diff(preset_str, effective_instructions, from_branch, to_branch)
737 .await
738}
739
740async fn generate_commit_review(
742 service: &Arc<CommitService>,
743 spinner: &indicatif::ProgressBar,
744 random_message: &messages::ColoredMessage,
745 preset_str: &str,
746 effective_instructions: &str,
747 commit_id: &str,
748) -> Result<GeneratedReview> {
749 spinner.set_message(format!(
750 "{} - Reviewing commit: {}",
751 random_message.text, commit_id
752 ));
753
754 service
755 .generate_review_for_commit(preset_str, effective_instructions, commit_id)
756 .await
757}
758
759async fn generate_working_directory_review(
761 service: &Arc<CommitService>,
762 spinner: &indicatif::ProgressBar,
763 random_message: &messages::ColoredMessage,
764 preset_str: &str,
765 effective_instructions: &str,
766 include_unstaged: bool,
767) -> Result<GeneratedReview> {
768 if include_unstaged {
769 spinner.set_message(format!(
770 "{} - Including unstaged changes",
771 random_message.text
772 ));
773
774 let git_info = service.get_git_info_with_unstaged(include_unstaged).await?;
776
777 if git_info.staged_files.is_empty() {
778 spinner.finish_and_clear();
779 ui::print_warning("No changes found (staged or unstaged). Nothing to review.");
780 return Err(anyhow::anyhow!("No changes to review"));
781 }
782
783 service
785 .generate_review_with_unstaged(preset_str, effective_instructions, include_unstaged)
786 .await
787 } else {
788 let git_info = service.get_git_info().await?;
790
791 if git_info.staged_files.is_empty() {
792 spinner.finish_and_clear();
793 ui::print_warning(
794 "No staged changes. Please stage your changes before generating a review.",
795 );
796 ui::print_info("You can stage changes using 'git add <file>' or 'git add .'");
797 ui::print_info("To include unstaged changes, use --include-unstaged");
798 ui::print_info(
799 "To review differences between branches, use --from and --to (--from defaults to 'main')",
800 );
801 return Err(anyhow::anyhow!("No staged changes to review"));
802 }
803
804 service
806 .generate_review(preset_str, effective_instructions)
807 .await
808 }
809}