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