agcodex_core/subagents/built_in/
refactorer.rs

1//! Refactorer Agent - Performs systematic code refactoring
2//!
3//! This agent analyzes code structure and performs safe refactoring operations:
4//! - Extract method/function
5//! - Rename variables and functions
6//! - Remove duplicate code
7//! - Simplify complex expressions
8//! - Apply design patterns
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 std::collections::HashMap;
23use std::future::Future;
24use std::path::Path;
25use std::path::PathBuf;
26use std::pin::Pin;
27use std::sync::Arc;
28use std::sync::atomic::AtomicBool;
29use std::sync::atomic::Ordering;
30use std::time::Duration;
31use std::time::SystemTime;
32
33/// Refactorer Agent implementation
34#[derive(Debug)]
35pub struct RefactorerAgent {
36    name: String,
37    description: String,
38    _mode_override: Option<OperatingMode>,
39    _tool_permissions: Vec<String>,
40    _prompt_template: String,
41    risk_tolerance: RiskLevel,
42}
43
44/// Risk level for refactoring operations
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum RiskLevel {
47    Conservative, // Only safe, guaranteed refactorings
48    Moderate,     // Standard refactorings with good test coverage
49    Aggressive,   // Complex refactorings that may require manual review
50}
51
52impl Default for RefactorerAgent {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl RefactorerAgent {
59    /// Create a new refactorer agent
60    pub fn new() -> Self {
61        Self {
62            name: "refactorer".to_string(),
63            description: "Performs systematic code refactoring with risk assessment".to_string(),
64            _mode_override: Some(OperatingMode::Build),
65            _tool_permissions: vec![
66                "search".to_string(),
67                "edit".to_string(),
68                "patch".to_string(),
69                "tree".to_string(),
70                "grep".to_string(),
71            ],
72            _prompt_template: r#"
73You are an expert refactoring specialist focused on:
74- Improving code structure without changing behavior
75- Reducing complexity and duplication
76- Applying SOLID principles
77- Enhancing readability and maintainability
78- Ensuring backwards compatibility
79
80Always verify that refactorings preserve functionality.
81Create comprehensive test cases before major changes.
82"#
83            .to_string(),
84            risk_tolerance: RiskLevel::Moderate,
85        }
86    }
87
88    /// Set risk tolerance level
89    pub const fn with_risk_level(mut self, level: RiskLevel) -> Self {
90        self.risk_tolerance = level;
91        self
92    }
93
94    /// Identify refactoring opportunities
95    async fn identify_refactoring_opportunities(
96        &self,
97        ast_tools: &mut ASTAgentTools,
98        file: &Path,
99    ) -> Vec<Finding> {
100        let mut findings = Vec::new();
101
102        // Check for duplicate code
103        if let Ok(AgentToolResult::DuplicateCode(duplicates)) =
104            ast_tools.execute(AgentToolOp::FindDuplicateCode {
105                min_lines: 5,
106                similarity_threshold: 0.85,
107            })
108        {
109            for duplicate in duplicates {
110                findings.push(Finding {
111                    category: "refactoring".to_string(),
112                    severity: Severity::Medium,
113                    title: "Duplicate Code Detected".to_string(),
114                    description: format!(
115                        "Found {} duplicate code blocks with {} lines each",
116                        duplicate.locations.len(),
117                        duplicate.line_count
118                    ),
119                    location: duplicate.locations.first().cloned(),
120                    suggestion: Some("Extract duplicate code into a shared function".to_string()),
121                    metadata: HashMap::from([
122                        (
123                            "refactoring_type".to_string(),
124                            serde_json::json!("extract_method"),
125                        ),
126                        (
127                            "duplicate_count".to_string(),
128                            serde_json::json!(duplicate.locations.len()),
129                        ),
130                        (
131                            "line_count".to_string(),
132                            serde_json::json!(duplicate.line_count),
133                        ),
134                    ]),
135                });
136            }
137        }
138
139        // Check for long parameter lists
140        if let Ok(AgentToolResult::Functions(functions)) =
141            ast_tools.execute(AgentToolOp::ExtractFunctions {
142                file: file.to_path_buf(),
143                language: self.detect_language(file),
144            })
145        {
146            for func in functions {
147                if func.parameters.len() > 4 {
148                    findings.push(Finding {
149                        category: "refactoring".to_string(),
150                        severity: Severity::Low,
151                        title: format!("Long Parameter List: {}", func.name),
152                        description: format!(
153                            "Function '{}' has {} parameters. Consider using a parameter object.",
154                            func.name,
155                            func.parameters.len()
156                        ),
157                        location: Some(crate::code_tools::ast_agent_tools::Location {
158                            file: file.to_path_buf(),
159                            line: func.start_line,
160                            column: 0,
161                            byte_offset: 0,
162                        }),
163                        suggestion: Some(
164                            "Introduce a parameter object or builder pattern".to_string(),
165                        ),
166                        metadata: HashMap::from([
167                            (
168                                "refactoring_type".to_string(),
169                                serde_json::json!("introduce_parameter_object"),
170                            ),
171                            (
172                                "parameter_count".to_string(),
173                                serde_json::json!(func.parameters.len()),
174                            ),
175                        ]),
176                    });
177                }
178            }
179        }
180
181        findings
182    }
183
184    /// Apply safe refactorings
185    async fn apply_refactorings(
186        &self,
187        ast_tools: &mut ASTAgentTools,
188        findings: &[Finding],
189    ) -> Vec<PathBuf> {
190        let mut modified_files = Vec::new();
191
192        for finding in findings {
193            // Only apply refactorings based on risk level
194            let should_apply = match self.risk_tolerance {
195                RiskLevel::Conservative => finding.severity == Severity::Low,
196                RiskLevel::Moderate => finding.severity != Severity::Critical,
197                RiskLevel::Aggressive => true,
198            };
199
200            if should_apply && let Some(refactoring_type) = finding.metadata.get("refactoring_type")
201            {
202                match refactoring_type.as_str() {
203                    Some("extract_method") => {
204                        // Apply extract method refactoring
205                        if let Some(location) = &finding.location
206                            && let Ok(AgentToolResult::Refactored(result)) =
207                                ast_tools.execute(AgentToolOp::RefactorExtractMethod {
208                                    location: location.clone(),
209                                    new_name: "extracted_function".to_string(),
210                                })
211                            && result.success
212                        {
213                            modified_files.push(location.file.clone());
214                        }
215                    }
216                    Some("introduce_parameter_object") => {
217                        // Apply parameter object refactoring
218                        if let Some(location) = &finding.location
219                            && let Ok(AgentToolResult::Refactored(result)) =
220                                ast_tools.execute(AgentToolOp::RefactorIntroduceParameterObject {
221                                    location: location.clone(),
222                                    object_name: "Parameters".to_string(),
223                                })
224                            && result.success
225                        {
226                            modified_files.push(location.file.clone());
227                        }
228                    }
229                    _ => {}
230                }
231            }
232        }
233
234        modified_files
235    }
236
237    /// Detect language from file extension
238    fn detect_language(&self, file: &Path) -> String {
239        file.extension()
240            .and_then(|ext| ext.to_str())
241            .map(|ext| match ext {
242                "rs" => "rust",
243                "py" => "python",
244                "js" | "jsx" => "javascript",
245                "ts" | "tsx" => "typescript",
246                "go" => "go",
247                "java" => "java",
248                _ => "text",
249            })
250            .unwrap_or("text")
251            .to_string()
252    }
253}
254
255impl Subagent for RefactorerAgent {
256    fn name(&self) -> &str {
257        &self.name
258    }
259
260    fn description(&self) -> &str {
261        &self.description
262    }
263
264    fn execute<'a>(
265        &'a self,
266        context: &'a SubagentContext,
267        ast_tools: &'a mut ASTAgentTools,
268        cancel_flag: Arc<AtomicBool>,
269    ) -> Pin<Box<dyn Future<Output = SubagentResult<AgentResult>> + Send + 'a>> {
270        Box::pin(async move {
271            let start_time = SystemTime::now();
272            let mut all_findings = Vec::new();
273            let mut analyzed_files = Vec::new();
274            let mut modified_files = Vec::new();
275
276            // Get files to refactor from context
277            let files = self.get_files_from_context(context)?;
278
279            for file in &files {
280                if cancel_flag.load(Ordering::Acquire) {
281                    return Err(SubagentError::ExecutionFailed(
282                        "Refactoring cancelled".to_string(),
283                    ));
284                }
285
286                analyzed_files.push(file.clone());
287
288                // Identify refactoring opportunities
289                let findings = self
290                    .identify_refactoring_opportunities(ast_tools, file)
291                    .await;
292                all_findings.extend(findings.clone());
293
294                // Apply refactorings if in Build mode
295                if context.mode == OperatingMode::Build {
296                    let modified = self.apply_refactorings(ast_tools, &findings).await;
297                    modified_files.extend(modified);
298                }
299            }
300
301            let summary = format!(
302                "Refactoring analysis completed: {} files analyzed, {} opportunities found, {} files modified",
303                analyzed_files.len(),
304                all_findings.len(),
305                modified_files.len()
306            );
307
308            // Store lengths before moving the values
309            let refactoring_opportunities = all_findings.len();
310            let files_modified = modified_files.len();
311
312            let execution_time = SystemTime::now()
313                .duration_since(start_time)
314                .unwrap_or_else(|_| Duration::from_secs(0));
315
316            Ok(AgentResult {
317                agent_name: self.name.clone(),
318                status: AgentStatus::Completed,
319                findings: all_findings,
320                analyzed_files,
321                modified_files,
322                execution_time,
323                summary,
324                metrics: HashMap::from([
325                    (
326                        "refactoring_opportunities".to_string(),
327                        serde_json::json!(refactoring_opportunities),
328                    ),
329                    (
330                        "files_modified".to_string(),
331                        serde_json::json!(files_modified),
332                    ),
333                    (
334                        "risk_level".to_string(),
335                        serde_json::json!(format!("{:?}", self.risk_tolerance)),
336                    ),
337                ]),
338            })
339        })
340    }
341
342    fn capabilities(&self) -> Vec<String> {
343        vec![
344            "code-refactoring".to_string(),
345            "duplicate-detection".to_string(),
346            "pattern-application".to_string(),
347            "complexity-reduction".to_string(),
348        ]
349    }
350
351    fn supports_file_type(&self, file_path: &Path) -> bool {
352        let supported = ["rs", "py", "js", "ts", "jsx", "tsx", "go", "java"];
353        file_path
354            .extension()
355            .and_then(|ext| ext.to_str())
356            .map(|ext| supported.contains(&ext))
357            .unwrap_or(false)
358    }
359
360    fn execution_time_estimate(&self) -> Duration {
361        Duration::from_secs(90)
362    }
363}
364
365impl RefactorerAgent {
366    fn get_files_from_context(
367        &self,
368        context: &SubagentContext,
369    ) -> Result<Vec<PathBuf>, SubagentError> {
370        if let Some(files) = context.parameters.get("files") {
371            Ok(files.split(',').map(|s| PathBuf::from(s.trim())).collect())
372        } else {
373            Ok(vec![context.working_directory.clone()])
374        }
375    }
376}