agcodex_core/subagents/built_in/
code_reviewer.rs

1//! Code Reviewer Agent - Reviews code for quality, security, and maintainability
2//!
3//! This agent performs comprehensive code review using AST analysis to identify:
4//! - Security vulnerabilities (OWASP Top 10)
5//! - Performance issues and bottlenecks
6//! - Code complexity and maintainability concerns
7//! - Best practice violations
8//! - Missing error handling
9
10use crate::code_tools::ast_agent_tools::ASTAgentTools;
11use crate::code_tools::ast_agent_tools::AgentToolOp;
12use crate::code_tools::ast_agent_tools::AgentToolResult;
13use crate::modes::OperatingMode;
14use crate::subagents::AgentResult;
15use crate::subagents::AgentStatus;
16use crate::subagents::Finding;
17use crate::subagents::Severity;
18use crate::subagents::Subagent;
19use crate::subagents::SubagentContext;
20use crate::subagents::SubagentError;
21use crate::subagents::SubagentResult;
22use serde::Deserialize;
23use serde::Serialize;
24use std::collections::HashMap;
25use std::future::Future;
26use std::path::Path;
27use std::path::PathBuf;
28use std::pin::Pin;
29use std::sync::Arc;
30use std::sync::atomic::AtomicBool;
31use std::sync::atomic::Ordering;
32use std::time::Duration;
33use std::time::SystemTime;
34
35/// Code Reviewer Agent implementation
36#[derive(Debug)]
37pub struct CodeReviewerAgent {
38    /// Agent name
39    name: String,
40    /// Agent description
41    description: String,
42    /// Operating mode override
43    _mode_override: Option<OperatingMode>,
44    /// Tool permissions
45    _tool_permissions: Vec<String>,
46    /// Custom prompt template
47    _prompt_template: String,
48    /// Intelligence level
49    intelligence_level: IntelligenceLevel,
50}
51
52/// Intelligence level for analysis depth
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54pub enum IntelligenceLevel {
55    Light,  // Quick surface-level review
56    Medium, // Standard review depth
57    Hard,   // Deep analysis with all checks
58}
59
60impl Default for CodeReviewerAgent {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl CodeReviewerAgent {
67    /// Create a new code reviewer agent
68    pub fn new() -> Self {
69        Self {
70            name: "code-reviewer".to_string(),
71            description: "Reviews code for quality, security, and maintainability".to_string(),
72            _mode_override: Some(OperatingMode::Review),
73            _tool_permissions: vec![
74                "search".to_string(),
75                "tree".to_string(),
76                "grep".to_string(),
77                "glob".to_string(),
78            ],
79            _prompt_template: r#"
80You are a senior code reviewer with expertise in:
81- Security vulnerability detection (OWASP Top 10)
82- Performance optimization
83- Code maintainability and readability
84- Design patterns and best practices
85- Error handling and edge cases
86
87Focus on finding actionable issues with clear remediation steps.
88Prioritize security and data integrity issues as Critical/High severity.
89"#
90            .to_string(),
91            intelligence_level: IntelligenceLevel::Medium,
92        }
93    }
94
95    /// Set intelligence level
96    pub const fn with_intelligence(mut self, level: IntelligenceLevel) -> Self {
97        self.intelligence_level = level;
98        self
99    }
100
101    /// Perform security analysis
102    async fn analyze_security(&self, ast_tools: &mut ASTAgentTools, file: &Path) -> Vec<Finding> {
103        let mut findings = Vec::new();
104
105        // Check for SQL injection vulnerabilities
106        if let Ok(AgentToolResult::Patterns(patterns)) =
107            ast_tools.execute(AgentToolOp::FindPatterns {
108                pattern_type: crate::code_tools::ast_agent_tools::PatternType::SqlInjection,
109                scope: crate::code_tools::search::SearchScope::Files(vec![file.to_path_buf()]),
110            })
111        {
112            for pattern in patterns {
113                findings.push(Finding {
114                    category: "security".to_string(),
115                    severity: Severity::Critical,
116                    title: "Potential SQL Injection Vulnerability".to_string(),
117                    description: format!(
118                        "Found potential SQL injection at {}:{}. User input may be directly concatenated into SQL query.",
119                        pattern.location.file.display(),
120                        pattern.location.line
121                    ),
122                    location: Some(pattern.location),
123                    suggestion: Some("Use parameterized queries or prepared statements instead of string concatenation".to_string()),
124                    metadata: HashMap::from([
125                        ("vulnerability_type".to_string(), serde_json::json!("sql_injection")),
126                        ("owasp_category".to_string(), serde_json::json!("A03:2021")),
127                    ]),
128                });
129            }
130        }
131
132        // Check for hardcoded secrets
133        if let Ok(AgentToolResult::Patterns(patterns)) =
134            ast_tools.execute(AgentToolOp::FindPatterns {
135                pattern_type: crate::code_tools::ast_agent_tools::PatternType::HardcodedSecrets,
136                scope: crate::code_tools::search::SearchScope::Files(vec![file.to_path_buf()]),
137            })
138        {
139            for pattern in patterns {
140                findings.push(Finding {
141                    category: "security".to_string(),
142                    severity: Severity::High,
143                    title: "Hardcoded Secret Detected".to_string(),
144                    description: format!(
145                        "Found hardcoded secret at {}:{}. This could lead to credential exposure.",
146                        pattern.location.file.display(),
147                        pattern.location.line
148                    ),
149                    location: Some(pattern.location),
150                    suggestion: Some(
151                        "Use environment variables or secure secret management systems".to_string(),
152                    ),
153                    metadata: HashMap::from([
154                        (
155                            "vulnerability_type".to_string(),
156                            serde_json::json!("hardcoded_secret"),
157                        ),
158                        ("owasp_category".to_string(), serde_json::json!("A07:2021")),
159                    ]),
160                });
161            }
162        }
163
164        findings
165    }
166
167    /// Analyze code complexity
168    async fn analyze_complexity(&self, ast_tools: &mut ASTAgentTools, file: &Path) -> Vec<Finding> {
169        let mut findings = Vec::new();
170
171        // Extract functions and analyze complexity
172        if let Ok(AgentToolResult::Functions(functions)) =
173            ast_tools.execute(AgentToolOp::ExtractFunctions {
174                file: file.to_path_buf(),
175                language: self.detect_language(file),
176            })
177        {
178            for func in functions {
179                // Check cyclomatic complexity
180                if let Ok(AgentToolResult::Complexity(complexity)) =
181                    ast_tools.execute(AgentToolOp::CalculateComplexity {
182                        function: func.name.clone(),
183                    })
184                {
185                    if complexity.cyclomatic_complexity > 10 {
186                        findings.push(Finding {
187                            category: "complexity".to_string(),
188                            severity: if complexity.cyclomatic_complexity > 20 {
189                                Severity::High
190                            } else {
191                                Severity::Medium
192                            },
193                            title: format!("High Complexity: {}", func.name),
194                            description: format!(
195                                "Function '{}' has cyclomatic complexity of {}. Complex functions are harder to test and maintain.",
196                                func.name, complexity.cyclomatic_complexity
197                            ),
198                            location: Some(crate::code_tools::ast_agent_tools::Location {
199                                file: file.to_path_buf(),
200                                line: func.start_line,
201                                column: 0,
202                                byte_offset: 0,
203                            }),
204                            suggestion: Some("Consider breaking this function into smaller, more focused functions".to_string()),
205                            metadata: HashMap::from([
206                                ("complexity".to_string(), serde_json::json!(complexity.cyclomatic_complexity)),
207                                ("threshold".to_string(), serde_json::json!(10)),
208                            ]),
209                        });
210                    }
211
212                    // Check cognitive complexity
213                    if complexity.cognitive_complexity > 15 {
214                        findings.push(Finding {
215                            category: "readability".to_string(),
216                            severity: Severity::Medium,
217                            title: format!("High Cognitive Complexity: {}", func.name),
218                            description: format!(
219                                "Function '{}' has cognitive complexity of {}. This makes the code harder to understand.",
220                                func.name, complexity.cognitive_complexity
221                            ),
222                            location: Some(crate::code_tools::ast_agent_tools::Location {
223                                file: file.to_path_buf(),
224                                line: func.start_line,
225                                column: 0,
226                                byte_offset: 0,
227                            }),
228                            suggestion: Some("Simplify control flow and reduce nesting levels".to_string()),
229                            metadata: HashMap::from([
230                                ("cognitive_complexity".to_string(), serde_json::json!(complexity.cognitive_complexity)),
231                                ("threshold".to_string(), serde_json::json!(15)),
232                            ]),
233                        });
234                    }
235                }
236
237                // Check function length
238                let line_count = func.end_line - func.start_line + 1;
239                if line_count > 50 {
240                    findings.push(Finding {
241                        category: "maintainability".to_string(),
242                        severity: if line_count > 100 { Severity::High } else { Severity::Medium },
243                        title: format!("Long Function: {}", func.name),
244                        description: format!(
245                            "Function '{}' is {} lines long. Long functions are harder to understand and test.",
246                            func.name, line_count
247                        ),
248                        location: Some(crate::code_tools::ast_agent_tools::Location {
249                            file: file.to_path_buf(),
250                            line: func.start_line,
251                            column: 0,
252                            byte_offset: 0,
253                        }),
254                        suggestion: Some("Extract cohesive blocks into separate functions".to_string()),
255                        metadata: HashMap::from([
256                            ("line_count".to_string(), serde_json::json!(line_count)),
257                            ("threshold".to_string(), serde_json::json!(50)),
258                        ]),
259                    });
260                }
261            }
262        }
263
264        findings
265    }
266
267    /// Check for performance issues
268    async fn analyze_performance(
269        &self,
270        ast_tools: &mut ASTAgentTools,
271        file: &Path,
272    ) -> Vec<Finding> {
273        let mut findings = Vec::new();
274
275        // Check for N+1 query patterns
276        if let Ok(AgentToolResult::Patterns(patterns)) =
277            ast_tools.execute(AgentToolOp::FindPatterns {
278                pattern_type: crate::code_tools::ast_agent_tools::PatternType::NPlusOneQuery,
279                scope: crate::code_tools::search::SearchScope::Files(vec![file.to_path_buf()]),
280            })
281        {
282            for pattern in patterns {
283                findings.push(Finding {
284                    category: "performance".to_string(),
285                    severity: Severity::High,
286                    title: "Potential N+1 Query Problem".to_string(),
287                    description: format!(
288                        "Found potential N+1 query pattern at {}:{}. This can cause severe performance issues.",
289                        pattern.location.file.display(),
290                        pattern.location.line
291                    ),
292                    location: Some(pattern.location),
293                    suggestion: Some("Use eager loading, batch queries, or data loaders to fetch related data efficiently".to_string()),
294                    metadata: HashMap::from([
295                        ("issue_type".to_string(), serde_json::json!("n_plus_one")),
296                    ]),
297                });
298            }
299        }
300
301        // Check for inefficient loops
302        if let Ok(AgentToolResult::Patterns(patterns)) =
303            ast_tools.execute(AgentToolOp::FindPatterns {
304                pattern_type: crate::code_tools::ast_agent_tools::PatternType::InefficientLoop,
305                scope: crate::code_tools::search::SearchScope::Files(vec![file.to_path_buf()]),
306            })
307        {
308            for pattern in patterns {
309                findings.push(Finding {
310                    category: "performance".to_string(),
311                    severity: Severity::Medium,
312                    title: "Inefficient Loop Pattern".to_string(),
313                    description: format!(
314                        "Found inefficient loop at {}:{}. Consider optimizing this loop for better performance.",
315                        pattern.location.file.display(),
316                        pattern.location.line
317                    ),
318                    location: Some(pattern.location),
319                    suggestion: Some("Consider using more efficient algorithms or data structures".to_string()),
320                    metadata: HashMap::from([
321                        ("issue_type".to_string(), serde_json::json!("inefficient_loop")),
322                    ]),
323                });
324            }
325        }
326
327        findings
328    }
329
330    /// Detect language from file extension
331    fn detect_language(&self, file: &Path) -> String {
332        file.extension()
333            .and_then(|ext| ext.to_str())
334            .map(|ext| match ext {
335                "rs" => "rust",
336                "py" => "python",
337                "js" | "jsx" => "javascript",
338                "ts" | "tsx" => "typescript",
339                "go" => "go",
340                "java" => "java",
341                "cpp" | "cc" | "cxx" => "cpp",
342                "c" => "c",
343                "rb" => "ruby",
344                _ => "text",
345            })
346            .unwrap_or("text")
347            .to_string()
348    }
349}
350
351impl Subagent for CodeReviewerAgent {
352    fn name(&self) -> &str {
353        &self.name
354    }
355
356    fn description(&self) -> &str {
357        &self.description
358    }
359
360    fn execute<'a>(
361        &'a self,
362        context: &'a SubagentContext,
363        ast_tools: &'a mut ASTAgentTools,
364        cancel_flag: Arc<AtomicBool>,
365    ) -> Pin<Box<dyn Future<Output = SubagentResult<AgentResult>> + Send + 'a>> {
366        Box::pin(async move {
367            let start_time = SystemTime::now();
368            let mut all_findings = Vec::new();
369            let mut analyzed_files = Vec::new();
370
371            // Get files to analyze from context
372            let files_to_analyze = self.get_files_to_analyze(context)?;
373
374            for file in &files_to_analyze {
375                if cancel_flag.load(Ordering::Acquire) {
376                    return Err(SubagentError::ExecutionFailed(
377                        "Review cancelled".to_string(),
378                    ));
379                }
380
381                analyzed_files.push(file.clone());
382
383                // Perform different types of analysis based on intelligence level
384                match self.intelligence_level {
385                    IntelligenceLevel::Light => {
386                        // Quick complexity check only
387                        let complexity_findings = self.analyze_complexity(ast_tools, file).await;
388                        all_findings.extend(complexity_findings);
389                    }
390                    IntelligenceLevel::Medium => {
391                        // Standard review: complexity + security
392                        let complexity_findings = self.analyze_complexity(ast_tools, file).await;
393                        let security_findings = self.analyze_security(ast_tools, file).await;
394                        all_findings.extend(complexity_findings);
395                        all_findings.extend(security_findings);
396                    }
397                    IntelligenceLevel::Hard => {
398                        // Deep analysis: all checks
399                        let complexity_findings = self.analyze_complexity(ast_tools, file).await;
400                        let security_findings = self.analyze_security(ast_tools, file).await;
401                        let performance_findings = self.analyze_performance(ast_tools, file).await;
402                        all_findings.extend(complexity_findings);
403                        all_findings.extend(security_findings);
404                        all_findings.extend(performance_findings);
405                    }
406                }
407            }
408
409            // Sort findings by severity
410            all_findings.sort_by(|a, b| a.severity.cmp(&b.severity));
411
412            // Generate summary
413            let critical_count = all_findings
414                .iter()
415                .filter(|f| f.severity == Severity::Critical)
416                .count();
417            let high_count = all_findings
418                .iter()
419                .filter(|f| f.severity == Severity::High)
420                .count();
421            let medium_count = all_findings
422                .iter()
423                .filter(|f| f.severity == Severity::Medium)
424                .count();
425            let low_count = all_findings
426                .iter()
427                .filter(|f| f.severity == Severity::Low)
428                .count();
429
430            // Store the length before moving all_findings
431            let total_findings = all_findings.len();
432
433            let summary = format!(
434                "Code review completed: {} files analyzed, {} findings (Critical: {}, High: {}, Medium: {}, Low: {})",
435                analyzed_files.len(),
436                total_findings,
437                critical_count,
438                high_count,
439                medium_count,
440                low_count
441            );
442
443            let execution_time = SystemTime::now()
444                .duration_since(start_time)
445                .unwrap_or_else(|_| Duration::from_secs(0));
446
447            Ok(AgentResult {
448                agent_name: self.name.clone(),
449                status: AgentStatus::Completed,
450                findings: all_findings,
451                analyzed_files,
452                modified_files: Vec::new(), // Code review doesn't modify files
453                execution_time,
454                summary,
455                metrics: HashMap::from([
456                    (
457                        "total_findings".to_string(),
458                        serde_json::json!(total_findings),
459                    ),
460                    (
461                        "critical_findings".to_string(),
462                        serde_json::json!(critical_count),
463                    ),
464                    ("high_findings".to_string(), serde_json::json!(high_count)),
465                    (
466                        "intelligence_level".to_string(),
467                        serde_json::json!(self.intelligence_level),
468                    ),
469                ]),
470            })
471        })
472    }
473
474    fn capabilities(&self) -> Vec<String> {
475        vec![
476            "ast-analysis".to_string(),
477            "security-scanning".to_string(),
478            "complexity-analysis".to_string(),
479            "performance-analysis".to_string(),
480            "best-practices".to_string(),
481        ]
482    }
483
484    fn supports_file_type(&self, file_path: &Path) -> bool {
485        let supported_extensions = [
486            "rs", "py", "js", "ts", "jsx", "tsx", "go", "java", "cpp", "c", "rb",
487        ];
488
489        file_path
490            .extension()
491            .and_then(|ext| ext.to_str())
492            .map(|ext| supported_extensions.contains(&ext))
493            .unwrap_or(false)
494    }
495
496    fn execution_time_estimate(&self) -> Duration {
497        match self.intelligence_level {
498            IntelligenceLevel::Light => Duration::from_secs(30),
499            IntelligenceLevel::Medium => Duration::from_secs(60),
500            IntelligenceLevel::Hard => Duration::from_secs(120),
501        }
502    }
503}
504
505impl CodeReviewerAgent {
506    /// Get files to analyze from context
507    fn get_files_to_analyze(
508        &self,
509        context: &SubagentContext,
510    ) -> Result<Vec<PathBuf>, SubagentError> {
511        // Extract file paths from context parameters
512        if let Some(files) = context.parameters.get("files") {
513            let paths: Vec<PathBuf> = files.split(',').map(|s| PathBuf::from(s.trim())).collect();
514            Ok(paths)
515        } else {
516            // Default to current directory
517            Ok(vec![context.working_directory.clone()])
518        }
519    }
520}