1use crate::errors::Result;
4use crate::models::PullRequest;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use tracing::{debug, info};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum IssueSeverity {
13 Critical,
15 Warning,
17 Info,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CodeQualityIssue {
24 pub severity: IssueSeverity,
26 pub title: String,
28 pub description: String,
30 pub file_path: String,
32 pub line_number: Option<u32>,
34 pub suggested_fix: Option<String>,
36}
37
38impl CodeQualityIssue {
39 pub fn new(
41 severity: IssueSeverity,
42 title: impl Into<String>,
43 description: impl Into<String>,
44 file_path: impl Into<String>,
45 ) -> Self {
46 Self {
47 severity,
48 title: title.into(),
49 description: description.into(),
50 file_path: file_path.into(),
51 line_number: None,
52 suggested_fix: None,
53 }
54 }
55
56 pub fn with_line_number(mut self, line: u32) -> Self {
58 self.line_number = Some(line);
59 self
60 }
61
62 pub fn with_suggested_fix(mut self, fix: impl Into<String>) -> Self {
64 self.suggested_fix = Some(fix.into());
65 self
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct CodeReviewSuggestion {
72 pub title: String,
74 pub body: String,
76 pub file_path: String,
78 pub line_number: Option<u32>,
80 pub is_critical: bool,
82}
83
84impl CodeReviewSuggestion {
85 pub fn new(
87 title: impl Into<String>,
88 body: impl Into<String>,
89 file_path: impl Into<String>,
90 ) -> Self {
91 Self {
92 title: title.into(),
93 body: body.into(),
94 file_path: file_path.into(),
95 line_number: None,
96 is_critical: false,
97 }
98 }
99
100 pub fn with_line_number(mut self, line: u32) -> Self {
102 self.line_number = Some(line);
103 self
104 }
105
106 pub fn as_critical(mut self) -> Self {
108 self.is_critical = true;
109 self
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct CodeReviewResult {
116 pub pr_number: u32,
118 pub issues: Vec<CodeQualityIssue>,
120 pub suggestions: Vec<CodeReviewSuggestion>,
122 pub quality_score: u32,
124 pub approved: bool,
126 pub approval_reason: Option<String>,
128}
129
130impl CodeReviewResult {
131 pub fn new(pr_number: u32) -> Self {
133 Self {
134 pr_number,
135 issues: Vec::new(),
136 suggestions: Vec::new(),
137 quality_score: 100,
138 approved: true,
139 approval_reason: None,
140 }
141 }
142
143 pub fn with_issue(mut self, issue: CodeQualityIssue) -> Self {
145 match issue.severity {
147 IssueSeverity::Critical => self.quality_score = self.quality_score.saturating_sub(20),
148 IssueSeverity::Warning => self.quality_score = self.quality_score.saturating_sub(10),
149 IssueSeverity::Info => self.quality_score = self.quality_score.saturating_sub(5),
150 }
151 self.issues.push(issue);
152 self
153 }
154
155 pub fn with_issues(mut self, issues: Vec<CodeQualityIssue>) -> Self {
157 for issue in issues {
158 self = self.with_issue(issue);
159 }
160 self
161 }
162
163 pub fn with_suggestion(mut self, suggestion: CodeReviewSuggestion) -> Self {
165 self.suggestions.push(suggestion);
166 self
167 }
168
169 pub fn with_suggestions(mut self, suggestions: Vec<CodeReviewSuggestion>) -> Self {
171 for suggestion in suggestions {
172 self = self.with_suggestion(suggestion);
173 }
174 self
175 }
176
177 pub fn set_approved(mut self, approved: bool, reason: Option<String>) -> Self {
179 self.approved = approved;
180 self.approval_reason = reason;
181 self
182 }
183
184 pub fn has_critical_issues(&self) -> bool {
186 self.issues.iter().any(|i| i.severity == IssueSeverity::Critical)
187 }
188
189 pub fn critical_issues_count(&self) -> usize {
191 self.issues.iter().filter(|i| i.severity == IssueSeverity::Critical).count()
192 }
193
194 pub fn warnings_count(&self) -> usize {
196 self.issues.iter().filter(|i| i.severity == IssueSeverity::Warning).count()
197 }
198
199 pub fn info_count(&self) -> usize {
201 self.issues.iter().filter(|i| i.severity == IssueSeverity::Info).count()
202 }
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct CodeReviewStandards {
208 pub min_quality_score: u32,
210 pub require_critical_fixes: bool,
212 pub require_warning_fixes: bool,
214 pub custom_rules: HashMap<String, String>,
216}
217
218impl Default for CodeReviewStandards {
219 fn default() -> Self {
220 Self {
221 min_quality_score: 70,
222 require_critical_fixes: true,
223 require_warning_fixes: false,
224 custom_rules: HashMap::new(),
225 }
226 }
227}
228
229impl CodeReviewStandards {
230 pub fn new(min_quality_score: u32) -> Self {
232 Self {
233 min_quality_score,
234 ..Default::default()
235 }
236 }
237
238 pub fn require_critical_fixes(mut self, require: bool) -> Self {
240 self.require_critical_fixes = require;
241 self
242 }
243
244 pub fn require_warning_fixes(mut self, require: bool) -> Self {
246 self.require_warning_fixes = require;
247 self
248 }
249
250 pub fn with_rule(mut self, name: impl Into<String>, rule: impl Into<String>) -> Self {
252 self.custom_rules.insert(name.into(), rule.into());
253 self
254 }
255}
256
257pub struct CodeReviewAgent {
259 pub standards: CodeReviewStandards,
261}
262
263impl CodeReviewAgent {
264 pub fn new() -> Self {
266 Self {
267 standards: CodeReviewStandards::default(),
268 }
269 }
270
271 pub fn with_standards(standards: CodeReviewStandards) -> Self {
273 Self { standards }
274 }
275
276 pub fn analyze_code(&self, pr: &PullRequest) -> Result<Vec<CodeQualityIssue>> {
278 debug!(
279 pr_number = pr.number,
280 file_count = pr.files.len(),
281 "Analyzing PR code for quality issues"
282 );
283
284 let mut issues = Vec::new();
285
286 for file in &pr.files {
288 if file.additions + file.deletions > 500 {
290 issues.push(
291 CodeQualityIssue::new(
292 IssueSeverity::Warning,
293 "Large file change",
294 format!(
295 "File {} has {} lines changed, consider breaking into smaller changes",
296 file.path,
297 file.additions + file.deletions
298 ),
299 &file.path,
300 )
301 );
302 }
303
304 if file.deletions > file.additions * 2 {
306 issues.push(
307 CodeQualityIssue::new(
308 IssueSeverity::Info,
309 "Large deletion",
310 format!(
311 "File {} has significant deletions ({} lines)",
312 file.path, file.deletions
313 ),
314 &file.path,
315 )
316 );
317 }
318 }
319
320 if pr.body.is_empty() {
322 issues.push(
323 CodeQualityIssue::new(
324 IssueSeverity::Warning,
325 "Missing PR description",
326 "PR body is empty, please provide a description of changes",
327 "PR",
328 )
329 );
330 }
331
332 if pr.title.len() > 100 {
334 issues.push(
335 CodeQualityIssue::new(
336 IssueSeverity::Info,
337 "Long PR title",
338 format!("PR title is {} characters, consider shortening", pr.title.len()),
339 "PR",
340 )
341 );
342 }
343
344 info!(
345 pr_number = pr.number,
346 issue_count = issues.len(),
347 "Code analysis complete"
348 );
349
350 Ok(issues)
351 }
352
353 pub fn generate_suggestions(&self, issues: &[CodeQualityIssue]) -> Result<Vec<CodeReviewSuggestion>> {
355 debug!(
356 issue_count = issues.len(),
357 "Generating code review suggestions"
358 );
359
360 let mut suggestions = Vec::new();
361
362 for issue in issues {
363 let suggestion = CodeReviewSuggestion::new(
364 &issue.title,
365 &issue.description,
366 &issue.file_path,
367 )
368 .with_line_number(issue.line_number.unwrap_or(0));
369
370 let suggestion = if issue.severity == IssueSeverity::Critical {
371 suggestion.as_critical()
372 } else {
373 suggestion
374 };
375
376 suggestions.push(suggestion);
377 }
378
379 info!(
380 suggestion_count = suggestions.len(),
381 "Suggestions generated"
382 );
383
384 Ok(suggestions)
385 }
386
387 pub fn validate_standards(&self, pr: &PullRequest) -> Result<Vec<CodeQualityIssue>> {
389 debug!(
390 pr_number = pr.number,
391 "Validating code against project standards"
392 );
393
394 let mut issues = Vec::new();
395
396 if pr.files.is_empty() {
398 issues.push(
399 CodeQualityIssue::new(
400 IssueSeverity::Critical,
401 "No files changed",
402 "PR has no file changes",
403 "PR",
404 )
405 );
406 }
407
408 if !self.is_valid_branch_name(&pr.branch) {
410 issues.push(
411 CodeQualityIssue::new(
412 IssueSeverity::Warning,
413 "Invalid branch name",
414 format!(
415 "Branch name '{}' does not follow naming conventions (use feature/, bugfix/, hotfix/)",
416 pr.branch
417 ),
418 "PR",
419 )
420 );
421 }
422
423 for rule_name in self.standards.custom_rules.keys() {
425 debug!(rule = rule_name, "Applying custom rule");
426 }
427
428 info!(
429 pr_number = pr.number,
430 issue_count = issues.len(),
431 "Standards validation complete"
432 );
433
434 Ok(issues)
435 }
436
437 pub fn is_valid_branch_name(&self, branch: &str) -> bool {
439 branch.starts_with("feature/")
441 || branch.starts_with("bugfix/")
442 || branch.starts_with("hotfix/")
443 || branch.starts_with("release/")
444 || branch == "main"
445 || branch == "develop"
446 || branch == "master"
447 }
448
449 pub fn generate_summary(&self, result: &CodeReviewResult) -> Result<String> {
451 debug!(
452 pr_number = result.pr_number,
453 quality_score = result.quality_score,
454 "Generating code review summary"
455 );
456
457 let mut summary = format!(
458 "## Code Review Summary\n\n**PR #{}**\n\n**Quality Score: {}/100**\n\n",
459 result.pr_number, result.quality_score
460 );
461
462 if result.approved {
464 summary.push_str("✅ **APPROVED**\n\n");
465 if let Some(reason) = &result.approval_reason {
466 summary.push_str(&format!("Reason: {}\n\n", reason));
467 }
468 } else {
469 summary.push_str("❌ **NEEDS REVIEW**\n\n");
470 if let Some(reason) = &result.approval_reason {
471 summary.push_str(&format!("Reason: {}\n\n", reason));
472 }
473 }
474
475 if !result.issues.is_empty() {
477 summary.push_str("### Issues Found\n\n");
478 summary.push_str(&format!(
479 "- **Critical**: {}\n",
480 result.critical_issues_count()
481 ));
482 summary.push_str(&format!("- **Warnings**: {}\n", result.warnings_count()));
483 summary.push_str(&format!("- **Info**: {}\n\n", result.info_count()));
484
485 let critical: Vec<_> = result
487 .issues
488 .iter()
489 .filter(|i| i.severity == IssueSeverity::Critical)
490 .collect();
491 if !critical.is_empty() {
492 summary.push_str("#### Critical Issues\n\n");
493 for issue in critical {
494 summary.push_str(&format!(
495 "- **{}** ({}): {}\n",
496 issue.title, issue.file_path, issue.description
497 ));
498 }
499 summary.push('\n');
500 }
501 }
502
503 if !result.suggestions.is_empty() {
505 summary.push_str("### Suggestions\n\n");
506 for suggestion in &result.suggestions {
507 summary.push_str(&format!(
508 "- **{}** ({}): {}\n",
509 suggestion.title, suggestion.file_path, suggestion.body
510 ));
511 }
512 }
513
514 info!(
515 pr_number = result.pr_number,
516 summary_length = summary.len(),
517 "Summary generated"
518 );
519
520 Ok(summary)
521 }
522
523 pub fn should_approve(&self, result: &CodeReviewResult) -> Result<bool> {
525 debug!(
526 pr_number = result.pr_number,
527 quality_score = result.quality_score,
528 "Determining approval status"
529 );
530
531 if result.quality_score < self.standards.min_quality_score {
533 return Ok(false);
534 }
535
536 if self.standards.require_critical_fixes && result.has_critical_issues() {
538 return Ok(false);
539 }
540
541 if self.standards.require_warning_fixes && result.warnings_count() > 0 {
543 return Ok(false);
544 }
545
546 Ok(true)
547 }
548
549 pub fn review_pr(&self, pr: &PullRequest) -> Result<CodeReviewResult> {
551 debug!(pr_number = pr.number, "Starting complete code review");
552
553 let code_issues = self.analyze_code(pr)?;
555
556 let standard_issues = self.validate_standards(pr)?;
558
559 let mut all_issues = code_issues;
561 all_issues.extend(standard_issues);
562
563 let suggestions = self.generate_suggestions(&all_issues)?;
565
566 let mut result = CodeReviewResult::new(pr.number)
568 .with_issues(all_issues);
569
570 result = result.with_suggestions(suggestions);
572
573 let should_approve = self.should_approve(&result)?;
575 let approval_reason = Some(format!(
576 "Code quality score is {} (minimum: {})",
577 result.quality_score, self.standards.min_quality_score
578 ));
579
580 result = result.set_approved(should_approve, approval_reason);
581
582 info!(
583 pr_number = pr.number,
584 approved = result.approved,
585 quality_score = result.quality_score,
586 "Code review complete"
587 );
588
589 Ok(result)
590 }
591}
592
593impl Default for CodeReviewAgent {
594 fn default() -> Self {
595 Self::new()
596 }
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602 use crate::models::{FileChange, PrStatus};
603
604 fn create_test_pr() -> PullRequest {
605 PullRequest {
606 id: 1,
607 number: 123,
608 title: "Test PR".to_string(),
609 body: "This is a test PR".to_string(),
610 branch: "feature/test".to_string(),
611 base: "main".to_string(),
612 status: PrStatus::Open,
613 files: vec![FileChange {
614 path: "src/main.rs".to_string(),
615 change_type: "modified".to_string(),
616 additions: 50,
617 deletions: 10,
618 }],
619 created_at: chrono::Utc::now(),
620 updated_at: chrono::Utc::now(),
621 }
622 }
623
624 #[test]
625 fn test_code_quality_issue_creation() {
626 let issue = CodeQualityIssue::new(
627 IssueSeverity::Warning,
628 "Test issue",
629 "This is a test",
630 "test.rs",
631 );
632 assert_eq!(issue.severity, IssueSeverity::Warning);
633 assert_eq!(issue.title, "Test issue");
634 assert_eq!(issue.file_path, "test.rs");
635 }
636
637 #[test]
638 fn test_code_quality_issue_with_line_number() {
639 let issue = CodeQualityIssue::new(
640 IssueSeverity::Critical,
641 "Test",
642 "Description",
643 "test.rs",
644 )
645 .with_line_number(42);
646 assert_eq!(issue.line_number, Some(42));
647 }
648
649 #[test]
650 fn test_code_review_suggestion_creation() {
651 let suggestion = CodeReviewSuggestion::new("Test", "Body", "test.rs");
652 assert_eq!(suggestion.title, "Test");
653 assert_eq!(suggestion.body, "Body");
654 assert!(!suggestion.is_critical);
655 }
656
657 #[test]
658 fn test_code_review_suggestion_as_critical() {
659 let suggestion = CodeReviewSuggestion::new("Test", "Body", "test.rs").as_critical();
660 assert!(suggestion.is_critical);
661 }
662
663 #[test]
664 fn test_code_review_result_creation() {
665 let result = CodeReviewResult::new(123);
666 assert_eq!(result.pr_number, 123);
667 assert_eq!(result.quality_score, 100);
668 assert!(result.approved);
669 }
670
671 #[test]
672 fn test_code_review_result_with_issue() {
673 let issue = CodeQualityIssue::new(
674 IssueSeverity::Critical,
675 "Test",
676 "Description",
677 "test.rs",
678 );
679 let result = CodeReviewResult::new(123).with_issue(issue);
680 assert_eq!(result.issues.len(), 1);
681 assert_eq!(result.critical_issues_count(), 1);
682 assert!(result.quality_score < 100);
683 }
684
685 #[test]
686 fn test_code_review_result_quality_score_calculation() {
687 let critical = CodeQualityIssue::new(
688 IssueSeverity::Critical,
689 "Critical",
690 "Description",
691 "test.rs",
692 );
693 let warning = CodeQualityIssue::new(
694 IssueSeverity::Warning,
695 "Warning",
696 "Description",
697 "test.rs",
698 );
699 let result = CodeReviewResult::new(123)
700 .with_issue(critical)
701 .with_issue(warning);
702 assert_eq!(result.quality_score, 70); }
704
705 #[test]
706 fn test_code_review_standards_default() {
707 let standards = CodeReviewStandards::default();
708 assert_eq!(standards.min_quality_score, 70);
709 assert!(standards.require_critical_fixes);
710 assert!(!standards.require_warning_fixes);
711 }
712
713 #[test]
714 fn test_code_review_agent_creation() {
715 let agent = CodeReviewAgent::new();
716 assert_eq!(agent.standards.min_quality_score, 70);
717 }
718
719 #[test]
720 fn test_analyze_code_empty_files() {
721 let agent = CodeReviewAgent::new();
722 let mut pr = create_test_pr();
723 pr.files.clear();
724 let issues = agent.analyze_code(&pr).unwrap();
725 assert!(issues.is_empty());
726 }
727
728 #[test]
729 fn test_analyze_code_large_file() {
730 let agent = CodeReviewAgent::new();
731 let mut pr = create_test_pr();
732 pr.files[0].additions = 300;
733 pr.files[0].deletions = 300;
734 let issues = agent.analyze_code(&pr).unwrap();
735 assert!(!issues.is_empty());
736 assert!(issues.iter().any(|i| i.title.contains("Large file")));
737 }
738
739 #[test]
740 fn test_analyze_code_empty_body() {
741 let agent = CodeReviewAgent::new();
742 let mut pr = create_test_pr();
743 pr.body.clear();
744 let issues = agent.analyze_code(&pr).unwrap();
745 assert!(issues.iter().any(|i| i.title.contains("Missing PR description")));
746 }
747
748 #[test]
749 fn test_validate_standards_no_files() {
750 let agent = CodeReviewAgent::new();
751 let mut pr = create_test_pr();
752 pr.files.clear();
753 let issues = agent.validate_standards(&pr).unwrap();
754 assert!(issues.iter().any(|i| i.severity == IssueSeverity::Critical));
755 }
756
757 #[test]
758 fn test_validate_standards_invalid_branch() {
759 let agent = CodeReviewAgent::new();
760 let mut pr = create_test_pr();
761 pr.branch = "invalid-branch".to_string();
762 let issues = agent.validate_standards(&pr).unwrap();
763 assert!(issues.iter().any(|i| i.title.contains("Invalid branch")));
764 }
765
766 #[test]
767 fn test_is_valid_branch_name() {
768 let agent = CodeReviewAgent::new();
769 assert!(agent.is_valid_branch_name("feature/test"));
770 assert!(agent.is_valid_branch_name("bugfix/test"));
771 assert!(agent.is_valid_branch_name("hotfix/test"));
772 assert!(agent.is_valid_branch_name("main"));
773 assert!(!agent.is_valid_branch_name("invalid"));
774 }
775
776 #[test]
777 fn test_generate_suggestions() {
778 let agent = CodeReviewAgent::new();
779 let issues = vec![CodeQualityIssue::new(
780 IssueSeverity::Warning,
781 "Test",
782 "Description",
783 "test.rs",
784 )];
785 let suggestions = agent.generate_suggestions(&issues).unwrap();
786 assert_eq!(suggestions.len(), 1);
787 assert_eq!(suggestions[0].title, "Test");
788 }
789
790 #[test]
791 fn test_generate_summary() {
792 let agent = CodeReviewAgent::new();
793 let result = CodeReviewResult::new(123);
794 let summary = agent.generate_summary(&result).unwrap();
795 assert!(summary.contains("Code Review Summary"));
796 assert!(summary.contains("PR #123"));
797 assert!(summary.contains("APPROVED"));
798 }
799
800 #[test]
801 fn test_should_approve_high_quality() {
802 let agent = CodeReviewAgent::new();
803 let result = CodeReviewResult::new(123).set_approved(true, None);
804 assert!(agent.should_approve(&result).unwrap());
805 }
806
807 #[test]
808 fn test_should_approve_low_quality() {
809 let agent = CodeReviewAgent::new();
810 let mut result = CodeReviewResult::new(123);
811 result.quality_score = 50;
812 assert!(!agent.should_approve(&result).unwrap());
813 }
814
815 #[test]
816 fn test_should_approve_with_critical_issues() {
817 let standards = CodeReviewStandards::default().require_critical_fixes(true);
818 let agent = CodeReviewAgent::with_standards(standards);
819 let issue = CodeQualityIssue::new(
820 IssueSeverity::Critical,
821 "Critical",
822 "Description",
823 "test.rs",
824 );
825 let result = CodeReviewResult::new(123).with_issue(issue);
826 assert!(!agent.should_approve(&result).unwrap());
827 }
828
829 #[test]
830 fn test_review_pr_complete() {
831 let agent = CodeReviewAgent::new();
832 let pr = create_test_pr();
833 let result = agent.review_pr(&pr).unwrap();
834 assert_eq!(result.pr_number, 123);
835 assert!(result.quality_score > 0);
836 }
837
838 #[test]
839 fn test_review_pr_with_issues() {
840 let agent = CodeReviewAgent::new();
841 let mut pr = create_test_pr();
842 pr.body.clear();
843 pr.branch = "invalid".to_string();
844 let result = agent.review_pr(&pr).unwrap();
845 assert!(!result.issues.is_empty());
846 }
847}