1use 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#[derive(Debug)]
37pub struct CodeReviewerAgent {
38 name: String,
40 description: String,
42 _mode_override: Option<OperatingMode>,
44 _tool_permissions: Vec<String>,
46 _prompt_template: String,
48 intelligence_level: IntelligenceLevel,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54pub enum IntelligenceLevel {
55 Light, Medium, Hard, }
59
60impl Default for CodeReviewerAgent {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl CodeReviewerAgent {
67 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 pub const fn with_intelligence(mut self, level: IntelligenceLevel) -> Self {
97 self.intelligence_level = level;
98 self
99 }
100
101 async fn analyze_security(&self, ast_tools: &mut ASTAgentTools, file: &Path) -> Vec<Finding> {
103 let mut findings = Vec::new();
104
105 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 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 async fn analyze_complexity(&self, ast_tools: &mut ASTAgentTools, file: &Path) -> Vec<Finding> {
169 let mut findings = Vec::new();
170
171 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 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 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 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 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 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 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 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 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 match self.intelligence_level {
385 IntelligenceLevel::Light => {
386 let complexity_findings = self.analyze_complexity(ast_tools, file).await;
388 all_findings.extend(complexity_findings);
389 }
390 IntelligenceLevel::Medium => {
391 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 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 all_findings.sort_by(|a, b| a.severity.cmp(&b.severity));
411
412 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 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(), 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 fn get_files_to_analyze(
508 &self,
509 context: &SubagentContext,
510 ) -> Result<Vec<PathBuf>, SubagentError> {
511 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 Ok(vec![context.working_directory.clone()])
518 }
519 }
520}