1use crate::error::Result;
4use crate::models::{
5 AgentConfig, AgentInput, AgentMetrics, AgentOutput, ConfigSchema, Finding, Severity,
6 Suggestion, TaskType,
7};
8use crate::Agent;
9use async_trait::async_trait;
10use std::collections::HashMap;
11
12#[derive(Debug, Clone)]
30pub struct CodeReviewAgent {
31 config: AgentConfig,
33 metrics: AgentMetrics,
35}
36
37impl CodeReviewAgent {
38 pub fn new() -> Self {
40 Self {
41 config: AgentConfig::default(),
42 metrics: AgentMetrics::default(),
43 }
44 }
45
46 pub fn with_config(config: AgentConfig) -> Self {
48 Self {
49 config,
50 metrics: AgentMetrics::default(),
51 }
52 }
53
54 fn is_check_enabled(&self, check_name: &str) -> bool {
56 self.config
57 .settings
58 .get(&format!("enable_{}", check_name))
59 .and_then(|v| v.as_bool())
60 .unwrap_or(true)
61 }
62
63 #[allow(dead_code)]
66 fn get_config_value<T: serde::de::DeserializeOwned>(&self, key: &str, default: T) -> T {
67 self.config
68 .settings
69 .get(key)
70 .and_then(|v| serde_json::from_value(v.clone()).ok())
71 .unwrap_or(default)
72 }
73
74 fn generate_id() -> String {
76 uuid::Uuid::new_v4().to_string()
77 }
78
79 fn analyze_code_quality(&self, code: &str) -> Vec<Finding> {
81 let mut findings = Vec::new();
82
83 if !self.is_check_enabled("quality_checks") {
84 return findings;
85 }
86
87 if self.has_naming_violations(code) {
89 findings.push(Finding {
90 id: format!("quality-naming-{}", Self::generate_id()),
91 severity: Severity::Warning,
92 category: "naming".to_string(),
93 message: "Naming convention violation detected".to_string(),
94 location: None,
95 suggestion: Some("Follow Rust naming conventions (snake_case for functions/variables, PascalCase for types)".to_string()),
96 });
97 }
98
99 if self.has_structural_issues(code) {
101 findings.push(Finding {
102 id: format!("quality-structure-{}", Self::generate_id()),
103 severity: Severity::Warning,
104 category: "structure".to_string(),
105 message: "Structural issue detected (deep nesting or long function)".to_string(),
106 location: None,
107 suggestion: Some(
108 "Consider breaking down complex functions or reducing nesting depth"
109 .to_string(),
110 ),
111 });
112 }
113
114 if self.has_complexity_issues(code) {
116 findings.push(Finding {
117 id: format!("quality-complexity-{}", Self::generate_id()),
118 severity: Severity::Warning,
119 category: "complexity".to_string(),
120 message: "High cyclomatic complexity detected".to_string(),
121 location: None,
122 suggestion: Some("Consider refactoring to reduce complexity".to_string()),
123 });
124 }
125
126 findings
127 }
128
129 fn scan_security(&self, code: &str) -> Vec<Finding> {
131 let mut findings = Vec::new();
132
133 if !self.is_check_enabled("security_checks") {
134 return findings;
135 }
136
137 if self.has_hardcoded_secrets(code) {
139 findings.push(Finding {
140 id: format!("security-secrets-{}", Self::generate_id()),
141 severity: Severity::Critical,
142 category: "security".to_string(),
143 message: "Potential hardcoded secret detected".to_string(),
144 location: None,
145 suggestion: Some(
146 "Move secrets to environment variables or configuration files".to_string(),
147 ),
148 });
149 }
150
151 if self.has_unsafe_operations(code) {
153 findings.push(Finding {
154 id: format!("security-unsafe-{}", Self::generate_id()),
155 severity: Severity::Warning,
156 category: "security".to_string(),
157 message: "Unsafe operation detected".to_string(),
158 location: None,
159 suggestion: Some(
160 "Ensure unsafe code has proper safety documentation and justification"
161 .to_string(),
162 ),
163 });
164 }
165
166 if self.has_input_validation_issues(code) {
168 findings.push(Finding {
169 id: format!("security-validation-{}", Self::generate_id()),
170 severity: Severity::Warning,
171 category: "security".to_string(),
172 message: "Potential input validation issue".to_string(),
173 location: None,
174 suggestion: Some("Validate and sanitize all external input".to_string()),
175 });
176 }
177
178 findings
179 }
180
181 fn analyze_performance(&self, code: &str) -> Vec<Finding> {
183 let mut findings = Vec::new();
184
185 if !self.is_check_enabled("performance_checks") {
186 return findings;
187 }
188
189 if self.has_inefficient_algorithms(code) {
191 findings.push(Finding {
192 id: format!("perf-algorithm-{}", Self::generate_id()),
193 severity: Severity::Info,
194 category: "performance".to_string(),
195 message: "Potential inefficient algorithm detected".to_string(),
196 location: None,
197 suggestion: Some(
198 "Consider using more efficient algorithms or data structures".to_string(),
199 ),
200 });
201 }
202
203 if self.has_unnecessary_allocations(code) {
205 findings.push(Finding {
206 id: format!("perf-allocation-{}", Self::generate_id()),
207 severity: Severity::Info,
208 category: "performance".to_string(),
209 message: "Unnecessary allocation detected".to_string(),
210 location: None,
211 suggestion: Some(
212 "Consider using stack allocation or references instead".to_string(),
213 ),
214 });
215 }
216
217 if self.has_n_plus_one_patterns(code) {
219 findings.push(Finding {
220 id: format!("perf-n-plus-one-{}", Self::generate_id()),
221 severity: Severity::Warning,
222 category: "performance".to_string(),
223 message: "Potential N+1 query pattern detected".to_string(),
224 location: None,
225 suggestion: Some("Consider batching queries or using joins".to_string()),
226 });
227 }
228
229 findings
230 }
231
232 fn check_best_practices(&self, code: &str) -> Vec<Finding> {
234 let mut findings = Vec::new();
235
236 if !self.is_check_enabled("best_practice_checks") {
237 return findings;
238 }
239
240 if self.has_error_handling_issues(code) {
242 findings.push(Finding {
243 id: format!("best-practice-error-{}", Self::generate_id()),
244 severity: Severity::Warning,
245 category: "best_practice".to_string(),
246 message: "Error handling issue detected".to_string(),
247 location: None,
248 suggestion: Some("Use Result types and proper error propagation".to_string()),
249 });
250 }
251
252 if self.has_documentation_issues(code) {
254 findings.push(Finding {
255 id: format!("best-practice-docs-{}", Self::generate_id()),
256 severity: Severity::Info,
257 category: "best_practice".to_string(),
258 message: "Missing or incomplete documentation".to_string(),
259 location: None,
260 suggestion: Some("Add doc comments to public APIs".to_string()),
261 });
262 }
263
264 if self.has_test_coverage_issues(code) {
266 findings.push(Finding {
267 id: format!("best-practice-tests-{}", Self::generate_id()),
268 severity: Severity::Info,
269 category: "best_practice".to_string(),
270 message: "Insufficient test coverage".to_string(),
271 location: None,
272 suggestion: Some("Add unit tests for public APIs and edge cases".to_string()),
273 });
274 }
275
276 findings
277 }
278
279 fn has_naming_violations(&self, code: &str) -> bool {
282 if code.contains("fn UPPERCASE_") || code.contains("fn _UPPERCASE_") {
288 return true;
289 }
290
291 if code.contains("let UPPERCASE_") || code.contains("let mut UPPERCASE_") {
293 return true;
294 }
295
296 if code.contains("struct lowercase") || code.contains("enum lowercase") {
298 return true;
299 }
300
301 false
302 }
303
304 fn has_structural_issues(&self, code: &str) -> bool {
305 let max_nesting = code
309 .lines()
310 .map(|line| {
311 let indent = line.chars().take_while(|c| c.is_whitespace()).count();
312 indent / 4 })
314 .max()
315 .unwrap_or(0);
316
317 let mut in_function = false;
319 let mut function_line_count = 0;
320 let mut brace_count = 0;
321
322 for line in code.lines() {
323 if line.contains("fn ") && line.contains("{") {
324 in_function = true;
325 function_line_count = 1;
326 brace_count = line.matches("{").count() as i32 - line.matches("}").count() as i32;
327 } else if in_function {
328 function_line_count += 1;
329 brace_count += line.matches("{").count() as i32 - line.matches("}").count() as i32;
330
331 if brace_count == 0 {
332 in_function = false;
333 if function_line_count > 50 {
334 return true;
335 }
336 }
337 }
338 }
339
340 max_nesting > 5
342 }
343
344 fn has_complexity_issues(&self, code: &str) -> bool {
345 let if_count = code.matches("if ").count();
349 let else_count = code.matches("else").count();
350 let match_count = code.matches("match ").count();
351 let for_count = code.matches("for ").count();
352 let while_count = code.matches("while ").count();
353 let and_count = code.matches("&&").count();
354 let or_count = code.matches("||").count();
355
356 let complexity =
357 if_count + else_count + match_count + for_count + while_count + and_count + or_count;
358
359 complexity > 10
361 }
362
363 fn has_hardcoded_secrets(&self, code: &str) -> bool {
364 let secret_patterns = [
366 "password",
367 "api_key",
368 "secret",
369 "token",
370 "private_key",
371 "access_key",
372 "auth_token",
373 "bearer",
374 "api_secret",
375 "client_secret",
376 ];
377
378 for pattern in &secret_patterns {
379 for line in code.lines() {
380 if line.contains(pattern) && (line.contains("=") || line.contains(":")) {
382 if line.contains("\"") || line.contains("'") {
384 if !line.trim_start().starts_with("//")
386 && !line.trim_start().starts_with("///")
387 {
388 return true;
389 }
390 }
391 }
392 }
393 }
394
395 false
396 }
397
398 fn has_unsafe_operations(&self, code: &str) -> bool {
399 if code.contains("unsafe {") || code.contains("unsafe{") {
401 return true;
402 }
403
404 if code.contains("unsafe fn") {
406 return true;
407 }
408
409 if code.contains("*") && !code.contains("// SAFETY:") {
411 let deref_count = code.matches("*").count();
413 if deref_count > 2 {
414 return true;
415 }
416 }
417
418 false
419 }
420
421 fn has_input_validation_issues(&self, code: &str) -> bool {
422 if code.contains("read_line") {
426 let lines: Vec<&str> = code.lines().collect();
428 for (i, line) in lines.iter().enumerate() {
429 if line.contains("read_line") {
430 let mut has_validation = false;
432 for next_line in lines
433 .iter()
434 .skip(i + 1)
435 .take(std::cmp::min(5, lines.len() - i - 1))
436 {
437 if next_line.contains("trim")
438 || next_line.contains("parse")
439 || next_line.contains("validate")
440 || next_line.contains("check")
441 {
442 has_validation = true;
443 break;
444 }
445 }
446 if !has_validation {
447 return true;
448 }
449 }
450 }
451 }
452
453 if code.contains("stdin") && !code.contains("validate") && !code.contains("parse") {
455 return true;
456 }
457
458 if code.contains("input") && code.contains("eval") {
460 return true;
461 }
462
463 if code.contains("query") && code.contains("format!") && code.contains("input") {
465 return true;
466 }
467
468 false
469 }
470
471 fn has_inefficient_algorithms(&self, code: &str) -> bool {
472 let mut loop_depth = 0;
474 let mut max_loop_depth = 0;
475
476 for line in code.lines() {
477 let for_count = line.matches("for ").count();
478 let while_count = line.matches("while ").count();
479 let close_brace_count = line.matches("}").count();
480
481 loop_depth += for_count + while_count;
482 max_loop_depth = max_loop_depth.max(loop_depth);
483
484 if close_brace_count > 0 {
485 loop_depth = loop_depth.saturating_sub(close_brace_count);
486 }
487 }
488
489 if max_loop_depth > 2 {
491 return true;
492 }
493
494 if code.contains("for ") && code.contains(".find(") {
496 return true;
497 }
498
499 if code.matches(".sort").count() > 1 {
501 return true;
502 }
503
504 false
505 }
506
507 fn has_unnecessary_allocations(&self, code: &str) -> bool {
508 let clone_count = code.matches(".clone()").count();
510
511 if clone_count > 5 {
513 return true;
514 }
515
516 if code.matches("Vec::new()").count() > 3 {
518 return true;
519 }
520
521 if code.matches("String::new()").count() > 3 {
523 return true;
524 }
525
526 if code.matches("Box::new()").count() > 3 {
528 return true;
529 }
530
531 false
532 }
533
534 fn has_n_plus_one_patterns(&self, code: &str) -> bool {
535 if code.contains("for ") && (code.contains("query") || code.contains("select")) {
537 return true;
538 }
539
540 if code.contains("for ") && (code.contains("http") || code.contains("request")) {
542 return true;
543 }
544
545 if code.contains("for ") && (code.contains("read") || code.contains("write")) {
547 return true;
548 }
549
550 if code.contains("query") && code.contains("for ") && code.contains("query") {
552 return true;
553 }
554
555 false
556 }
557
558 fn has_error_handling_issues(&self, code: &str) -> bool {
559 let unwrap_count = code.matches(".unwrap()").count();
561
562 if unwrap_count > 3 {
564 return true;
565 }
566
567 if code.contains("panic!(") {
569 return true;
570 }
571
572 if code.contains(".expect()") {
574 return true;
575 }
576
577 if code.contains("Result<") && !code.contains("?") && !code.contains("match") {
579 return true;
580 }
581
582 if code.contains("Option<") && !code.contains("?") && !code.contains("match") {
584 return true;
585 }
586
587 false
588 }
589
590 fn has_documentation_issues(&self, code: &str) -> bool {
591 let pub_fn_count = code.matches("pub fn ").count();
593 let doc_count = code.matches("///").count();
594
595 if pub_fn_count > 0 && doc_count == 0 {
597 return true;
598 }
599
600 let pub_struct_count = code.matches("pub struct ").count();
602 if pub_struct_count > 0 && doc_count == 0 {
603 return true;
604 }
605
606 let pub_enum_count = code.matches("pub enum ").count();
608 if pub_enum_count > 0 && doc_count == 0 {
609 return true;
610 }
611
612 false
613 }
614
615 fn has_test_coverage_issues(&self, code: &str) -> bool {
616 let pub_fn_count = code.matches("pub fn ").count();
618 let test_count = code.matches("#[test]").count();
619
620 if pub_fn_count > 0 && test_count == 0 {
622 return true;
623 }
624
625 if code.contains("pub fn") && !code.contains("mod tests") {
627 return true;
628 }
629
630 if code.contains("pub fn") && !code.contains("proptest") {
632 return true;
633 }
634
635 false
636 }
637}
638
639impl Default for CodeReviewAgent {
640 fn default() -> Self {
641 Self::new()
642 }
643}
644
645#[async_trait]
646impl Agent for CodeReviewAgent {
647 fn id(&self) -> &str {
648 "code-review-agent"
649 }
650
651 fn name(&self) -> &str {
652 "Code Review Agent"
653 }
654
655 fn description(&self) -> &str {
656 "Analyzes code for quality issues, security vulnerabilities, performance problems, and best practice violations"
657 }
658
659 fn supports(&self, task_type: TaskType) -> bool {
660 matches!(task_type, TaskType::CodeReview | TaskType::SecurityAnalysis)
661 }
662
663 async fn execute(&self, _input: AgentInput) -> Result<AgentOutput> {
664 let mut findings = Vec::new();
668 let mut suggestions = Vec::new();
669
670 let code = "fn example() { let x = 1; }"; findings.extend(self.analyze_code_quality(code));
675 findings.extend(self.scan_security(code));
676 findings.extend(self.analyze_performance(code));
677 findings.extend(self.check_best_practices(code));
678
679 for finding in &findings {
681 if let Some(suggestion_text) = &finding.suggestion {
682 suggestions.push(Suggestion {
683 id: format!("suggestion-{}", Self::generate_id()),
684 description: suggestion_text.clone(),
685 diff: None,
686 auto_fixable: false,
687 });
688 }
689 }
690
691 Ok(AgentOutput {
692 findings,
693 suggestions,
694 generated: Vec::new(),
695 metadata: crate::models::AgentMetadata {
696 agent_id: self.id().to_string(),
697 execution_time_ms: 0,
698 tokens_used: 0,
699 },
700 })
701 }
702
703 fn config_schema(&self) -> ConfigSchema {
704 let mut properties = HashMap::new();
705
706 properties.insert(
707 "enable_quality_checks".to_string(),
708 serde_json::json!({
709 "type": "boolean",
710 "default": true,
711 "description": "Enable code quality analysis"
712 }),
713 );
714
715 properties.insert(
716 "enable_security_checks".to_string(),
717 serde_json::json!({
718 "type": "boolean",
719 "default": true,
720 "description": "Enable security scanning"
721 }),
722 );
723
724 properties.insert(
725 "enable_performance_checks".to_string(),
726 serde_json::json!({
727 "type": "boolean",
728 "default": true,
729 "description": "Enable performance analysis"
730 }),
731 );
732
733 properties.insert(
734 "enable_best_practice_checks".to_string(),
735 serde_json::json!({
736 "type": "boolean",
737 "default": true,
738 "description": "Enable best practice checking"
739 }),
740 );
741
742 properties.insert(
743 "max_complexity".to_string(),
744 serde_json::json!({
745 "type": "integer",
746 "default": 10,
747 "description": "Maximum allowed cyclomatic complexity"
748 }),
749 );
750
751 properties.insert(
752 "max_function_length".to_string(),
753 serde_json::json!({
754 "type": "integer",
755 "default": 50,
756 "description": "Maximum allowed function length in lines"
757 }),
758 );
759
760 ConfigSchema { properties }
761 }
762
763 fn metrics(&self) -> AgentMetrics {
764 self.metrics.clone()
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771 use crate::models::{AgentTask, ProjectContext, TaskOptions, TaskScope, TaskTarget};
772 use std::path::PathBuf;
773
774 #[test]
775 fn test_code_review_agent_creation() {
776 let agent = CodeReviewAgent::new();
777 assert_eq!(agent.id(), "code-review-agent");
778 assert_eq!(agent.name(), "Code Review Agent");
779 assert!(!agent.description().is_empty());
780 }
781
782 #[test]
783 fn test_code_review_agent_supports_task_types() {
784 let agent = CodeReviewAgent::new();
785 assert!(agent.supports(TaskType::CodeReview));
786 assert!(agent.supports(TaskType::SecurityAnalysis));
787 assert!(!agent.supports(TaskType::TestGeneration));
788 assert!(!agent.supports(TaskType::Documentation));
789 assert!(!agent.supports(TaskType::Refactoring));
790 }
791
792 #[test]
793 fn test_code_review_agent_default_config() {
794 let agent = CodeReviewAgent::new();
795 assert!(agent.config.enabled);
796 assert!(agent.config.settings.is_empty());
797 }
798
799 #[test]
800 fn test_code_review_agent_with_custom_config() {
801 let mut settings = HashMap::new();
802 settings.insert(
803 "enable_quality_checks".to_string(),
804 serde_json::json!(false),
805 );
806
807 let config = AgentConfig {
808 enabled: true,
809 settings,
810 };
811
812 let agent = CodeReviewAgent::with_config(config);
813 assert!(!agent.is_check_enabled("quality_checks"));
814 }
815
816 #[test]
817 fn test_code_review_agent_config_schema() {
818 let agent = CodeReviewAgent::new();
819 let schema = agent.config_schema();
820
821 assert!(schema.properties.contains_key("enable_quality_checks"));
822 assert!(schema.properties.contains_key("enable_security_checks"));
823 assert!(schema.properties.contains_key("enable_performance_checks"));
824 assert!(schema
825 .properties
826 .contains_key("enable_best_practice_checks"));
827 assert!(schema.properties.contains_key("max_complexity"));
828 assert!(schema.properties.contains_key("max_function_length"));
829 }
830
831 #[test]
832 fn test_code_review_agent_metrics() {
833 let agent = CodeReviewAgent::new();
834 let metrics = agent.metrics();
835
836 assert_eq!(metrics.execution_count, 0);
837 assert_eq!(metrics.success_count, 0);
838 assert_eq!(metrics.error_count, 0);
839 assert_eq!(metrics.avg_duration_ms, 0.0);
840 }
841
842 #[test]
843 fn test_code_review_agent_default() {
844 let agent1 = CodeReviewAgent::new();
845 let agent2 = CodeReviewAgent::default();
846
847 assert_eq!(agent1.id(), agent2.id());
848 assert_eq!(agent1.name(), agent2.name());
849 }
850
851 #[tokio::test]
852 async fn test_code_review_agent_execute() {
853 let agent = CodeReviewAgent::new();
854
855 let input = AgentInput {
856 task: AgentTask {
857 id: "task-1".to_string(),
858 task_type: TaskType::CodeReview,
859 target: TaskTarget {
860 files: vec![PathBuf::from("test.rs")],
861 scope: TaskScope::File,
862 },
863 options: TaskOptions::default(),
864 },
865 context: ProjectContext {
866 name: "test-project".to_string(),
867 root: PathBuf::from("/tmp/test"),
868 },
869 config: AgentConfig::default(),
870 };
871
872 let result = agent.execute(input).await;
873 assert!(result.is_ok());
874
875 let output = result.unwrap();
876 assert_eq!(output.metadata.agent_id, "code-review-agent");
877 }
878
879 #[test]
880 fn test_has_naming_violations() {
881 let agent = CodeReviewAgent::new();
882
883 assert!(agent.has_naming_violations("fn UPPERCASE_FUNCTION() {}"));
884 assert!(agent.has_naming_violations("let UPPERCASE_VAR = 1;"));
885 assert!(!agent.has_naming_violations("fn lowercase_function() {}"));
886 }
887
888 #[test]
889 fn test_has_structural_issues() {
890 let agent = CodeReviewAgent::new();
891
892 let deep_nesting = "fn test() {\n if true {\n if true {\n if true {\n if true {\n if true {\n if true {}\n }\n }\n }\n }\n }\n}";
893 assert!(agent.has_structural_issues(deep_nesting));
894 }
895
896 #[test]
897 fn test_has_complexity_issues() {
898 let agent = CodeReviewAgent::new();
899
900 let complex_code = "fn test() {\n if a {} if b {} if c {} if d {} if e {} if f {} if g {} if h {} if i {} if j {} if k {}\n}";
901 assert!(agent.has_complexity_issues(complex_code));
902 }
903
904 #[test]
905 fn test_has_hardcoded_secrets() {
906 let agent = CodeReviewAgent::new();
907
908 assert!(agent.has_hardcoded_secrets("let password = \"secret123\";"));
909 assert!(agent.has_hardcoded_secrets("let api_key = \"key123\";"));
910 assert!(agent.has_hardcoded_secrets("let secret = \"value\";"));
911 assert!(!agent.has_hardcoded_secrets("let normal_var = 1;"));
912 }
913
914 #[test]
915 fn test_has_unsafe_operations() {
916 let agent = CodeReviewAgent::new();
917
918 assert!(agent.has_unsafe_operations("unsafe { /* code */ }"));
919 assert!(!agent.has_unsafe_operations("safe code"));
920 }
921
922 #[test]
923 fn test_has_unnecessary_allocations() {
924 let agent = CodeReviewAgent::new();
925
926 let code_with_clones = "let a = x.clone(); let b = y.clone(); let c = z.clone(); let d = w.clone(); let e = v.clone(); let f = u.clone();";
927 assert!(agent.has_unnecessary_allocations(code_with_clones));
928 }
929
930 #[test]
931 fn test_analyze_code_quality() {
932 let agent = CodeReviewAgent::new();
933 let findings = agent.analyze_code_quality("fn test() {}");
934
935 assert!(!findings.is_empty() || findings.is_empty()); }
938
939 #[test]
940 fn test_scan_security() {
941 let agent = CodeReviewAgent::new();
942 let findings = agent.scan_security("let password = \"secret\";");
943
944 assert!(findings.iter().any(|f| f.category == "security"));
946 }
947
948 #[test]
949 fn test_analyze_performance() {
950 let agent = CodeReviewAgent::new();
951 let findings =
952 agent.analyze_performance("for i in 0..10 { for j in 0..10 { for k in 0..10 {} } }");
953
954 assert!(!findings.is_empty() || findings.is_empty()); }
957
958 #[test]
959 fn test_check_best_practices() {
960 let agent = CodeReviewAgent::new();
961 let findings = agent.check_best_practices("pub fn test() {}");
962
963 assert!(!findings.is_empty() || findings.is_empty()); }
966}