1use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use crate::agent::core::tools::agentic::{
13 AgenticContext, AgenticTool, AgenticToolResult, InteractionRole, ToolGoal,
14};
15use crate::agent::core::tools::ToolError;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct LanguageInfo {
20 pub language: String,
22 pub confidence: f64,
24 pub file_extension: String,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ComplexityMetrics {
31 pub lines_of_code: usize,
33 pub cyclomatic_complexity: usize,
35 pub function_count: usize,
37 pub max_function_lines: usize,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub enum ReviewStrategy {
44 Quick,
46 Standard,
48 Deep,
50}
51
52pub struct SmartCodeReviewTool {
54 name: String,
56 description: String,
58 language_patterns: HashMap<String, Vec<String>>,
60}
61
62impl Default for SmartCodeReviewTool {
63 fn default() -> Self {
64 let mut language_patterns = HashMap::new();
65 language_patterns.insert("rust".to_string(), vec![".rs".to_string()]);
66 language_patterns.insert("python".to_string(), vec![".py".to_string()]);
67 language_patterns.insert(
68 "javascript".to_string(),
69 vec![".js".to_string(), ".jsx".to_string()],
70 );
71 language_patterns.insert(
72 "typescript".to_string(),
73 vec![".ts".to_string(), ".tsx".to_string()],
74 );
75 language_patterns.insert("go".to_string(), vec![".go".to_string()]);
76 language_patterns.insert("java".to_string(), vec![".java".to_string()]);
77 language_patterns.insert("c".to_string(), vec![".c".to_string(), ".h".to_string()]);
78 language_patterns.insert(
79 "cpp".to_string(),
80 vec![".cpp".to_string(), ".hpp".to_string(), ".cc".to_string()],
81 );
82
83 Self {
84 name: "smart_code_review".to_string(),
85 description: "Autonomous code review tool that adapts its strategy based on code complexity and language".to_string(),
86 language_patterns,
87 }
88 }
89}
90
91impl SmartCodeReviewTool {
92 pub fn new() -> Self {
94 Self::default()
95 }
96
97 fn detect_language(&self, file_path: Option<&str>, content: &str) -> LanguageInfo {
99 if let Some(path) = file_path {
101 let path_lower = path.to_lowercase();
102 for (lang, extensions) in &self.language_patterns {
103 for ext in extensions {
104 if path_lower.ends_with(ext) {
105 return LanguageInfo {
106 language: lang.clone(),
107 confidence: 0.95,
108 file_extension: ext.clone(),
109 };
110 }
111 }
112 }
113 }
114
115 self.detect_language_from_content(content)
117 }
118
119 fn detect_language_from_content(&self, content: &str) -> LanguageInfo {
121 let content = content.trim();
122
123 if content.contains("fn ") && content.contains("use std::") {
125 return LanguageInfo {
126 language: "rust".to_string(),
127 confidence: 0.85,
128 file_extension: ".rs".to_string(),
129 };
130 }
131
132 if content.contains("def ") && (content.contains(":") && content.contains("import ")) {
134 return LanguageInfo {
135 language: "python".to_string(),
136 confidence: 0.85,
137 file_extension: ".py".to_string(),
138 };
139 }
140
141 if content.contains("const ") || content.contains("let ") || content.contains("function ") {
143 if content.contains(": ") && content.contains("interface ") {
144 return LanguageInfo {
145 language: "typescript".to_string(),
146 confidence: 0.80,
147 file_extension: ".ts".to_string(),
148 };
149 }
150 return LanguageInfo {
151 language: "javascript".to_string(),
152 confidence: 0.80,
153 file_extension: ".js".to_string(),
154 };
155 }
156
157 if content.contains("package ") && content.contains("func ") {
159 return LanguageInfo {
160 language: "go".to_string(),
161 confidence: 0.85,
162 file_extension: ".go".to_string(),
163 };
164 }
165
166 LanguageInfo {
168 language: "unknown".to_string(),
169 confidence: 0.0,
170 file_extension: ".txt".to_string(),
171 }
172 }
173
174 fn calculate_complexity(&self, content: &str) -> ComplexityMetrics {
176 let lines: Vec<&str> = content.lines().collect();
177 let lines_of_code = lines.len();
178
179 let function_keywords = ["fn ", "def ", "function ", "func "];
181 let function_count = lines
182 .iter()
183 .filter(|line| {
184 let trimmed = line.trim();
185 function_keywords.iter().any(|kw| trimmed.starts_with(kw))
186 })
187 .count();
188
189 let control_flow = [
191 "if ", "for ", "while ", "match ", "switch ", "?", "&&", "||",
192 ];
193 let cyclomatic_complexity = lines
194 .iter()
195 .map(|line| {
196 control_flow
197 .iter()
198 .map(|kw| line.matches(kw).count())
199 .sum::<usize>()
200 })
201 .sum::<usize>()
202 + 1; let max_function_lines = if function_count > 0 {
206 lines_of_code / function_count.max(1)
207 } else {
208 lines_of_code
209 };
210
211 ComplexityMetrics {
212 lines_of_code,
213 cyclomatic_complexity,
214 function_count,
215 max_function_lines,
216 }
217 }
218
219 fn determine_strategy(&self, metrics: &ComplexityMetrics) -> ReviewStrategy {
221 if metrics.lines_of_code < 50 && metrics.function_count <= 2 {
222 ReviewStrategy::Quick
223 } else if metrics.cyclomatic_complexity > 20 || metrics.lines_of_code > 500 {
224 ReviewStrategy::Deep
225 } else {
226 ReviewStrategy::Standard
227 }
228 }
229
230 fn quick_review(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
232 let mut issues = Vec::new();
233
234 if content.len() > 10000 {
236 issues.push("⚠️ File is quite long, consider splitting".to_string());
237 }
238
239 match lang.language.as_str() {
241 "rust" => {
242 if !content.contains("///") && !content.contains("//") {
243 issues.push("⚠️ No documentation comments found".to_string());
244 }
245 }
246 "python" => {
247 if !content.contains("\"\"\"") && !content.contains("#") {
248 issues.push("⚠️ No docstrings or comments found".to_string());
249 }
250 }
251 _ => {}
252 }
253
254 if issues.is_empty() {
255 issues.push("✅ Quick review passed".to_string());
256 }
257
258 issues
259 }
260
261 fn standard_review(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
263 let mut issues = self.quick_review(content, lang);
264
265 let lines: Vec<&str> = content.lines().collect();
267
268 let todo_count = lines
270 .iter()
271 .filter(|l| l.contains("TODO") || l.contains("FIXME"))
272 .count();
273 if todo_count > 0 {
274 issues.push(format!("📋 Found {} TODO/FIXME comments", todo_count));
275 }
276
277 let long_lines = lines
279 .iter()
280 .enumerate()
281 .filter(|(_, l)| l.len() > 120)
282 .count();
283 if long_lines > 0 {
284 issues.push(format!(
285 "📏 Found {} lines exceeding 120 characters",
286 long_lines
287 ));
288 }
289
290 match lang.language.as_str() {
292 "rust" => {
293 if content.contains("unwrap()") {
294 let unwrap_count = content.matches("unwrap()").count();
295 issues.push(format!(
296 "⚠️ Found {} uses of unwrap() - consider proper error handling",
297 unwrap_count
298 ));
299 }
300 if content.contains("panic!") {
301 issues.push("⚠️ Found panic! macro - ensure these are justified".to_string());
302 }
303 }
304 "python" => {
305 if content.contains("except:") && !content.contains("except ") {
306 issues.push("⚠️ Found bare except: - use specific exceptions".to_string());
307 }
308 }
309 _ => {}
310 }
311
312 issues
313 }
314
315 fn deep_review(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
317 let mut issues = self.standard_review(content, lang);
318
319 let lines: Vec<&str> = content.lines().collect();
321
322 let mut line_counts: HashMap<String, usize> = HashMap::new();
324 for line in &lines {
325 let trimmed = line.trim().to_string();
326 if trimmed.len() > 20 {
327 *line_counts.entry(trimmed).or_insert(0) += 1;
328 }
329 }
330 let duplicates: Vec<_> = line_counts.iter().filter(|(_, c)| **c > 2).collect();
331 if !duplicates.is_empty() {
332 issues.push(format!(
333 "🔍 Found {} potentially duplicated code blocks",
334 duplicates.len()
335 ));
336 }
337
338 let metrics = self.calculate_complexity(content);
340 if metrics.cyclomatic_complexity > 20 {
341 issues.push(format!(
342 "🚨 High cyclomatic complexity: {}. Consider refactoring into smaller functions",
343 metrics.cyclomatic_complexity
344 ));
345 }
346
347 let security_issues = self.check_security_issues(content, lang);
349 issues.extend(security_issues);
350
351 issues
352 }
353
354 fn check_security_issues(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
356 let mut issues = Vec::new();
357
358 match lang.language.as_str() {
359 "rust" => {
360 if content.contains("unsafe ") {
361 let unsafe_count = content.matches("unsafe ").count();
362 issues.push(format!(
363 "🚨 Found {} unsafe blocks - ensure memory safety is maintained",
364 unsafe_count
365 ));
366 }
367 }
368 "python" => {
369 if content.contains("eval(") {
370 issues.push("🚨 Found eval() - potential security risk".to_string());
371 }
372 if content.contains("exec(") {
373 issues.push("🚨 Found exec() - potential security risk".to_string());
374 }
375 }
376 "javascript" | "typescript" => {
377 if content.contains("eval(") {
378 issues.push("🚨 Found eval() - potential security risk".to_string());
379 }
380 if content.contains("innerHTML") {
381 issues.push("⚠️ Found innerHTML - potential XSS risk".to_string());
382 }
383 }
384 _ => {}
385 }
386
387 issues
388 }
389
390 fn has_critical_issues(&self, issues: &[String]) -> bool {
392 issues.iter().any(|i| i.starts_with("🚨"))
393 }
394
395 fn count_by_severity(&self, issues: &[String]) -> (usize, usize, usize) {
397 let critical = issues.iter().filter(|i| i.starts_with("🚨")).count();
398 let warning = issues.iter().filter(|i| i.starts_with("⚠️")).count();
399 let info = issues
400 .iter()
401 .filter(|i| {
402 i.starts_with("✅")
403 || i.starts_with("📋")
404 || i.starts_with("📏")
405 || i.starts_with("🔍")
406 })
407 .count();
408 (critical, warning, info)
409 }
410}
411
412#[async_trait]
413impl AgenticTool for SmartCodeReviewTool {
414 fn name(&self) -> &str {
415 &self.name
416 }
417
418 fn description(&self) -> &str {
419 &self.description
420 }
421
422 async fn execute(
423 &self,
424 goal: ToolGoal,
425 context: &mut AgenticContext,
426 ) -> Result<AgenticToolResult, ToolError> {
427 let file_path = goal.params.get("file_path").and_then(|v| v.as_str());
429 let content = goal
430 .params
431 .get("content")
432 .and_then(|v| v.as_str())
433 .ok_or_else(|| {
434 ToolError::InvalidArguments("Missing 'content' parameter".to_string())
435 })?;
436
437 context.record_interaction(
439 InteractionRole::System,
440 format!("Starting smart code review for goal: {}", goal.intent),
441 );
442
443 let language = self.detect_language(file_path, content);
445 context.record_interaction_with_metadata(
446 InteractionRole::Assistant,
447 format!(
448 "Detected language: {} (confidence: {})",
449 language.language, language.confidence
450 ),
451 serde_json::to_value(&language).unwrap_or_default(),
452 );
453
454 let complexity = self.calculate_complexity(content);
456 context.record_interaction_with_metadata(
457 InteractionRole::Assistant,
458 format!(
459 "Calculated complexity: {} lines, {} functions, complexity score {}",
460 complexity.lines_of_code,
461 complexity.function_count,
462 complexity.cyclomatic_complexity
463 ),
464 serde_json::to_value(&complexity).unwrap_or_default(),
465 );
466
467 let strategy = self.determine_strategy(&complexity);
469 let strategy_str = match strategy {
470 ReviewStrategy::Quick => "Quick",
471 ReviewStrategy::Standard => "Standard",
472 ReviewStrategy::Deep => "Deep",
473 };
474 context.record_interaction(
475 InteractionRole::Assistant,
476 format!("Selected review strategy: {}", strategy_str),
477 );
478
479 let issues = match strategy {
481 ReviewStrategy::Quick => self.quick_review(content, &language),
482 ReviewStrategy::Standard => self.standard_review(content, &language),
483 ReviewStrategy::Deep => self.deep_review(content, &language),
484 };
485
486 let (critical, warning, info) = self.count_by_severity(&issues);
488
489 if self.has_critical_issues(&issues) && context.is_first_iteration() {
490 let review_state = serde_json::json!({
492 "language": language,
493 "complexity": complexity,
494 "strategy": strategy_str,
495 "issues": issues,
496 "critical_count": critical,
497 "warning_count": warning,
498 "info_count": info,
499 });
500 context.update_state(review_state).await;
501
502 return Ok(AgenticToolResult::need_clarification_with_options(
504 format!(
505 "Found {} critical issue(s) and {} warning(s) in the code. \
506 The critical issues may require immediate attention. \
507 How would you like to proceed?",
508 critical, warning
509 ),
510 vec![
511 "Fix critical issues automatically (if possible)".to_string(),
512 "Show me detailed explanations of the issues".to_string(),
513 "Continue with current code (I understand the risks)".to_string(),
514 "Generate refactoring suggestions".to_string(),
515 ],
516 ));
517 }
518
519 let result = serde_json::json!({
521 "language": language,
522 "complexity": complexity,
523 "strategy": strategy_str,
524 "summary": {
525 "critical": critical,
526 "warning": warning,
527 "info": info,
528 "total": issues.len(),
529 },
530 "issues": issues,
531 });
532
533 context.update_state(result.clone()).await;
535
536 Ok(AgenticToolResult::success(
537 serde_json::to_string_pretty(&result).unwrap_or_default(),
538 ))
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545 use crate::agent::core::tools::agentic::{AgenticContext, ToolExecutor};
546 use crate::agent::core::tools::ToolCall;
547 use std::sync::Arc;
548
549 struct MockExecutor;
550
551 #[async_trait]
552 impl ToolExecutor for MockExecutor {
553 async fn execute(&self, _call: &ToolCall) -> Result<AgenticToolResult, ToolError> {
554 Ok(AgenticToolResult::success("mock"))
555 }
556 }
557
558 #[test]
559 fn test_language_detection_from_extension() {
560 let tool = SmartCodeReviewTool::new();
561
562 let lang = tool.detect_language(Some("test.rs"), "");
563 assert_eq!(lang.language, "rust");
564 assert_eq!(lang.confidence, 0.95);
565
566 let lang = tool.detect_language(Some("test.py"), "");
567 assert_eq!(lang.language, "python");
568 }
569
570 #[test]
571 fn test_language_detection_from_content() {
572 let tool = SmartCodeReviewTool::new();
573
574 let rust_code = r#"
575 use std::collections::HashMap;
576 fn main() {
577 println!("Hello");
578 }
579 "#;
580 let lang = tool.detect_language(None, rust_code);
581 assert_eq!(lang.language, "rust");
582
583 let python_code = r#"
584 import os
585 def main():
586 print("Hello")
587 "#;
588 let lang = tool.detect_language(None, python_code);
589 assert_eq!(lang.language, "python");
590 }
591
592 #[test]
593 fn test_complexity_calculation() {
594 let tool = SmartCodeReviewTool::new();
595
596 let code = r#"
597 fn main() {
598 if true {
599 for i in 0..10 {
600 while false {}
601 }
602 }
603 }
604 "#;
605
606 let metrics = tool.calculate_complexity(code);
607 assert!(metrics.lines_of_code > 0);
608 assert!(metrics.cyclomatic_complexity >= 4); }
610
611 #[test]
612 fn test_strategy_selection() {
613 let tool = SmartCodeReviewTool::new();
614
615 let simple = ComplexityMetrics {
616 lines_of_code: 30,
617 cyclomatic_complexity: 2,
618 function_count: 1,
619 max_function_lines: 30,
620 };
621 assert!(matches!(
622 tool.determine_strategy(&simple),
623 ReviewStrategy::Quick
624 ));
625
626 let complex = ComplexityMetrics {
627 lines_of_code: 600,
628 cyclomatic_complexity: 30,
629 function_count: 10,
630 max_function_lines: 60,
631 };
632 assert!(matches!(
633 tool.determine_strategy(&complex),
634 ReviewStrategy::Deep
635 ));
636 }
637
638 #[test]
639 fn test_security_issue_detection() {
640 let tool = SmartCodeReviewTool::new();
641
642 let rust_code = "unsafe { *ptr }";
643 let lang = LanguageInfo {
644 language: "rust".to_string(),
645 confidence: 1.0,
646 file_extension: ".rs".to_string(),
647 };
648 let issues = tool.check_security_issues(rust_code, &lang);
649 assert!(issues.iter().any(|i| i.contains("unsafe")));
650
651 let python_code = "eval(user_input)";
652 let lang = LanguageInfo {
653 language: "python".to_string(),
654 confidence: 1.0,
655 file_extension: ".py".to_string(),
656 };
657 let issues = tool.check_security_issues(python_code, &lang);
658 assert!(issues.iter().any(|i| i.contains("eval")));
659 }
660
661 #[tokio::test]
662 async fn test_smart_review_execution() {
663 let tool = SmartCodeReviewTool::new();
664 let executor: Arc<dyn ToolExecutor> = Arc::new(MockExecutor);
665 let mut context = AgenticContext::new(executor);
666
667 let goal = ToolGoal::new(
668 "Review this Rust code",
669 serde_json::json!({
670 "file_path": "test.rs",
671 "content": r#"
672 fn main() {
673 println!("Hello");
674 }
675 "#
676 }),
677 );
678
679 let result = tool.execute(goal, &mut context).await;
680 assert!(result.is_ok());
681
682 let agentic_result = result.unwrap();
683 assert!(agentic_result.is_success());
684
685 assert!(!context.interaction_history.is_empty());
687 }
688
689 #[tokio::test]
690 async fn test_critical_issues_trigger_clarification() {
691 let tool = SmartCodeReviewTool::new();
692 let executor: Arc<dyn ToolExecutor> = Arc::new(MockExecutor);
693 let mut context = AgenticContext::new(executor);
694
695 let goal = ToolGoal::new(
697 "Review this code with security issues",
698 serde_json::json!({
699 "file_path": "test.rs",
700 "content": r#"
701 // Line 1
702 // Line 2
703 // Line 3
704 // Line 4
705 // Line 5
706 unsafe fn dangerous() {
707 let ptr: *const i32 = std::ptr::null();
708 unsafe { *ptr }
709 }
710
711 fn complex_function(x: i32) -> i32 {
712 if x > 0 {
713 if x < 10 {
714 for i in 0..x {
715 while i < x {
716 if i == 5 {
717 return i;
718 }
719 }
720 }
721 }
722 }
723 x
724 }
725
726 fn another_complex(y: i32) -> i32 {
727 match y {
728 1 => 1,
729 2 => 2,
730 3 => 3,
731 4 => 4,
732 5 => 5,
733 _ => 0,
734 }
735 }
736 "#
737 }),
738 );
739
740 let result = tool.execute(goal, &mut context).await;
741 assert!(result.is_ok());
742
743 let agentic_result = result.unwrap();
744 assert!(
746 agentic_result.needs_clarification() || agentic_result.is_success(),
747 "Expected either clarification request (Deep strategy) or success (Standard strategy)"
748 );
749 }
750}