1use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use crate::tools::agentic::{
13 AgenticContext, AgenticTool, AgenticToolResult, InteractionRole, ToolGoal,
14};
15use crate::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" if !content.contains("///") && !content.contains("//") => {
242 issues.push("⚠️ No documentation comments found".to_string());
243 }
244 "python" if !content.contains("\"\"\"") && !content.contains("#") => {
245 issues.push("⚠️ No docstrings or comments found".to_string());
246 }
247 _ => {}
248 }
249
250 if issues.is_empty() {
251 issues.push("✅ Quick review passed".to_string());
252 }
253
254 issues
255 }
256
257 fn standard_review(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
259 let mut issues = self.quick_review(content, lang);
260
261 let lines: Vec<&str> = content.lines().collect();
263
264 let todo_count = lines
266 .iter()
267 .filter(|l| l.contains("TODO") || l.contains("FIXME"))
268 .count();
269 if todo_count > 0 {
270 issues.push(format!("📋 Found {} TODO/FIXME comments", todo_count));
271 }
272
273 let long_lines = lines
275 .iter()
276 .enumerate()
277 .filter(|(_, l)| l.len() > 120)
278 .count();
279 if long_lines > 0 {
280 issues.push(format!(
281 "📏 Found {} lines exceeding 120 characters",
282 long_lines
283 ));
284 }
285
286 match lang.language.as_str() {
288 "rust" => {
289 if content.contains("unwrap()") {
290 let unwrap_count = content.matches("unwrap()").count();
291 issues.push(format!(
292 "⚠️ Found {} uses of unwrap() - consider proper error handling",
293 unwrap_count
294 ));
295 }
296 if content.contains("panic!") {
297 issues.push("⚠️ Found panic! macro - ensure these are justified".to_string());
298 }
299 }
300 "python" if content.contains("except:") && !content.contains("except ") => {
301 issues.push("⚠️ Found bare except: - use specific exceptions".to_string());
302 }
303 _ => {}
304 }
305
306 issues
307 }
308
309 fn deep_review(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
311 let mut issues = self.standard_review(content, lang);
312
313 let lines: Vec<&str> = content.lines().collect();
315
316 let mut line_counts: HashMap<String, usize> = HashMap::new();
318 for line in &lines {
319 let trimmed = line.trim().to_string();
320 if trimmed.len() > 20 {
321 *line_counts.entry(trimmed).or_insert(0) += 1;
322 }
323 }
324 let duplicates: Vec<_> = line_counts.iter().filter(|(_, c)| **c > 2).collect();
325 if !duplicates.is_empty() {
326 issues.push(format!(
327 "🔍 Found {} potentially duplicated code blocks",
328 duplicates.len()
329 ));
330 }
331
332 let metrics = self.calculate_complexity(content);
334 if metrics.cyclomatic_complexity > 20 {
335 issues.push(format!(
336 "🚨 High cyclomatic complexity: {}. Consider refactoring into smaller functions",
337 metrics.cyclomatic_complexity
338 ));
339 }
340
341 let security_issues = self.check_security_issues(content, lang);
343 issues.extend(security_issues);
344
345 issues
346 }
347
348 fn check_security_issues(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
350 let mut issues = Vec::new();
351
352 match lang.language.as_str() {
353 "rust" if content.contains("unsafe ") => {
354 let unsafe_count = content.matches("unsafe ").count();
355 issues.push(format!(
356 "🚨 Found {} unsafe blocks - ensure memory safety is maintained",
357 unsafe_count
358 ));
359 }
360 "python" => {
361 if content.contains("eval(") {
362 issues.push("🚨 Found eval() - potential security risk".to_string());
363 }
364 if content.contains("exec(") {
365 issues.push("🚨 Found exec() - potential security risk".to_string());
366 }
367 }
368 "javascript" | "typescript" => {
369 if content.contains("eval(") {
370 issues.push("🚨 Found eval() - potential security risk".to_string());
371 }
372 if content.contains("innerHTML") {
373 issues.push("⚠️ Found innerHTML - potential XSS risk".to_string());
374 }
375 }
376 _ => {}
377 }
378
379 issues
380 }
381
382 fn has_critical_issues(&self, issues: &[String]) -> bool {
384 issues.iter().any(|i| i.starts_with("🚨"))
385 }
386
387 fn count_by_severity(&self, issues: &[String]) -> (usize, usize, usize) {
389 let critical = issues.iter().filter(|i| i.starts_with("🚨")).count();
390 let warning = issues.iter().filter(|i| i.starts_with("⚠️")).count();
391 let info = issues
392 .iter()
393 .filter(|i| {
394 i.starts_with("✅")
395 || i.starts_with("📋")
396 || i.starts_with("📏")
397 || i.starts_with("🔍")
398 })
399 .count();
400 (critical, warning, info)
401 }
402}
403
404#[async_trait]
405impl AgenticTool for SmartCodeReviewTool {
406 fn name(&self) -> &str {
407 &self.name
408 }
409
410 fn description(&self) -> &str {
411 &self.description
412 }
413
414 async fn execute(
415 &self,
416 goal: ToolGoal,
417 context: &mut AgenticContext,
418 ) -> Result<AgenticToolResult, ToolError> {
419 let file_path = goal.params.get("file_path").and_then(|v| v.as_str());
421 let content = goal
422 .params
423 .get("content")
424 .and_then(|v| v.as_str())
425 .ok_or_else(|| {
426 ToolError::InvalidArguments("Missing 'content' parameter".to_string())
427 })?;
428
429 context.record_interaction(
431 InteractionRole::System,
432 format!("Starting smart code review for goal: {}", goal.intent),
433 );
434
435 let language = self.detect_language(file_path, content);
437 context.record_interaction_with_metadata(
438 InteractionRole::Assistant,
439 format!(
440 "Detected language: {} (confidence: {})",
441 language.language, language.confidence
442 ),
443 serde_json::to_value(&language).unwrap_or_default(),
444 );
445
446 let complexity = self.calculate_complexity(content);
448 context.record_interaction_with_metadata(
449 InteractionRole::Assistant,
450 format!(
451 "Calculated complexity: {} lines, {} functions, complexity score {}",
452 complexity.lines_of_code,
453 complexity.function_count,
454 complexity.cyclomatic_complexity
455 ),
456 serde_json::to_value(&complexity).unwrap_or_default(),
457 );
458
459 let strategy = self.determine_strategy(&complexity);
461 let strategy_str = match strategy {
462 ReviewStrategy::Quick => "Quick",
463 ReviewStrategy::Standard => "Standard",
464 ReviewStrategy::Deep => "Deep",
465 };
466 context.record_interaction(
467 InteractionRole::Assistant,
468 format!("Selected review strategy: {}", strategy_str),
469 );
470
471 let issues = match strategy {
473 ReviewStrategy::Quick => self.quick_review(content, &language),
474 ReviewStrategy::Standard => self.standard_review(content, &language),
475 ReviewStrategy::Deep => self.deep_review(content, &language),
476 };
477
478 let (critical, warning, info) = self.count_by_severity(&issues);
480
481 if self.has_critical_issues(&issues) && context.is_first_iteration() {
482 let review_state = serde_json::json!({
484 "language": language,
485 "complexity": complexity,
486 "strategy": strategy_str,
487 "issues": issues,
488 "critical_count": critical,
489 "warning_count": warning,
490 "info_count": info,
491 });
492 context.update_state(review_state).await;
493
494 return Ok(AgenticToolResult::need_clarification_with_options(
496 format!(
497 "Found {} critical issue(s) and {} warning(s) in the code. \
498 The critical issues may require immediate attention. \
499 How would you like to proceed?",
500 critical, warning
501 ),
502 vec![
503 "Fix critical issues automatically (if possible)".to_string(),
504 "Show me detailed explanations of the issues".to_string(),
505 "Continue with current code (I understand the risks)".to_string(),
506 "Generate refactoring suggestions".to_string(),
507 ],
508 ));
509 }
510
511 let result = serde_json::json!({
513 "language": language,
514 "complexity": complexity,
515 "strategy": strategy_str,
516 "summary": {
517 "critical": critical,
518 "warning": warning,
519 "info": info,
520 "total": issues.len(),
521 },
522 "issues": issues,
523 });
524
525 context.update_state(result.clone()).await;
527
528 Ok(AgenticToolResult::success(
529 serde_json::to_string_pretty(&result).unwrap_or_default(),
530 ))
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537 use crate::tools::agentic::{AgenticContext, AgenticToolExecutor};
538 use crate::tools::ToolCall;
539 use std::sync::Arc;
540
541 struct MockExecutor;
542
543 #[async_trait]
544 impl AgenticToolExecutor for MockExecutor {
545 async fn execute(&self, _call: &ToolCall) -> Result<AgenticToolResult, ToolError> {
546 Ok(AgenticToolResult::success("mock"))
547 }
548 }
549
550 #[test]
551 fn test_language_detection_from_extension() {
552 let tool = SmartCodeReviewTool::new();
553
554 let lang = tool.detect_language(Some("test.rs"), "");
555 assert_eq!(lang.language, "rust");
556 assert_eq!(lang.confidence, 0.95);
557
558 let lang = tool.detect_language(Some("test.py"), "");
559 assert_eq!(lang.language, "python");
560 }
561
562 #[test]
563 fn test_language_detection_from_content() {
564 let tool = SmartCodeReviewTool::new();
565
566 let rust_code = r#"
567 use std::collections::HashMap;
568 fn main() {
569 println!("Hello");
570 }
571 "#;
572 let lang = tool.detect_language(None, rust_code);
573 assert_eq!(lang.language, "rust");
574
575 let python_code = r#"
576 import os
577 def main():
578 print("Hello")
579 "#;
580 let lang = tool.detect_language(None, python_code);
581 assert_eq!(lang.language, "python");
582 }
583
584 #[test]
585 fn test_complexity_calculation() {
586 let tool = SmartCodeReviewTool::new();
587
588 let code = r#"
589 fn main() {
590 if true {
591 for i in 0..10 {
592 while false {}
593 }
594 }
595 }
596 "#;
597
598 let metrics = tool.calculate_complexity(code);
599 assert!(metrics.lines_of_code > 0);
600 assert!(metrics.cyclomatic_complexity >= 4); }
602
603 #[test]
604 fn test_strategy_selection() {
605 let tool = SmartCodeReviewTool::new();
606
607 let simple = ComplexityMetrics {
608 lines_of_code: 30,
609 cyclomatic_complexity: 2,
610 function_count: 1,
611 max_function_lines: 30,
612 };
613 assert!(matches!(
614 tool.determine_strategy(&simple),
615 ReviewStrategy::Quick
616 ));
617
618 let complex = ComplexityMetrics {
619 lines_of_code: 600,
620 cyclomatic_complexity: 30,
621 function_count: 10,
622 max_function_lines: 60,
623 };
624 assert!(matches!(
625 tool.determine_strategy(&complex),
626 ReviewStrategy::Deep
627 ));
628 }
629
630 #[test]
631 fn test_security_issue_detection() {
632 let tool = SmartCodeReviewTool::new();
633
634 let rust_code = "unsafe { *ptr }";
635 let lang = LanguageInfo {
636 language: "rust".to_string(),
637 confidence: 1.0,
638 file_extension: ".rs".to_string(),
639 };
640 let issues = tool.check_security_issues(rust_code, &lang);
641 assert!(issues.iter().any(|i| i.contains("unsafe")));
642
643 let python_code = "eval(user_input)";
644 let lang = LanguageInfo {
645 language: "python".to_string(),
646 confidence: 1.0,
647 file_extension: ".py".to_string(),
648 };
649 let issues = tool.check_security_issues(python_code, &lang);
650 assert!(issues.iter().any(|i| i.contains("eval")));
651 }
652
653 #[tokio::test]
654 async fn test_smart_review_execution() {
655 let tool = SmartCodeReviewTool::new();
656 let executor: Arc<dyn AgenticToolExecutor> = Arc::new(MockExecutor);
657 let mut context = AgenticContext::new(executor);
658
659 let goal = ToolGoal::new(
660 "Review this Rust code",
661 serde_json::json!({
662 "file_path": "test.rs",
663 "content": r#"
664 fn main() {
665 println!("Hello");
666 }
667 "#
668 }),
669 );
670
671 let result = tool.execute(goal, &mut context).await;
672 assert!(result.is_ok());
673
674 let agentic_result = result.unwrap();
675 assert!(agentic_result.is_success());
676
677 assert!(!context.interaction_history.is_empty());
679 }
680
681 #[tokio::test]
682 async fn test_critical_issues_trigger_clarification() {
683 let tool = SmartCodeReviewTool::new();
684 let executor: Arc<dyn AgenticToolExecutor> = Arc::new(MockExecutor);
685 let mut context = AgenticContext::new(executor);
686
687 let goal = ToolGoal::new(
689 "Review this code with security issues",
690 serde_json::json!({
691 "file_path": "test.rs",
692 "content": r#"
693 // Line 1
694 // Line 2
695 // Line 3
696 // Line 4
697 // Line 5
698 unsafe fn dangerous() {
699 let ptr: *const i32 = std::ptr::null();
700 unsafe { *ptr }
701 }
702
703 fn complex_function(x: i32) -> i32 {
704 if x > 0 {
705 if x < 10 {
706 for i in 0..x {
707 while i < x {
708 if i == 5 {
709 return i;
710 }
711 }
712 }
713 }
714 }
715 x
716 }
717
718 fn another_complex(y: i32) -> i32 {
719 match y {
720 1 => 1,
721 2 => 2,
722 3 => 3,
723 4 => 4,
724 5 => 5,
725 _ => 0,
726 }
727 }
728 "#
729 }),
730 );
731
732 let result = tool.execute(goal, &mut context).await;
733 assert!(result.is_ok());
734
735 let agentic_result = result.unwrap();
736 assert!(
738 agentic_result.needs_clarification() || agentic_result.is_success(),
739 "Expected either clarification request (Deep strategy) or success (Standard strategy)"
740 );
741 }
742}