Skip to main content

cargo_fl/
autofix.rs

1use crate::rules::{Fix, Issue, Location, Replacement};
2use std::path::Path;
3use syn::{File as SynFile, Item, ItemUse, UseTree};
4
5pub struct AutoFixEngine {
6    pub fixes_applied: usize,
7}
8
9impl AutoFixEngine {
10    pub fn new() -> Self {
11        Self {
12            fixes_applied: 0,
13        }
14    }
15    
16    pub fn apply_fixes(&mut self, content: &str, issues: &[Issue]) -> Result<String, Box<dyn std::error::Error>> {
17        let mut fixed_content = content.to_string();
18        let mut offset_adjustment = 0i64;
19        
20        // Sort fixes by position (reverse order to maintain positions)
21        let mut fixes_with_positions: Vec<_> = issues
22            .iter()
23            .filter_map(|issue| issue.fix.as_ref().map(|fix| (issue, fix)))
24            .collect();
25        
26        // Sort by start position in reverse order
27        fixes_with_positions.sort_by_key(|(issue, _)| std::cmp::Reverse(issue.location.line));
28        
29        for (issue, fix) in fixes_with_positions {
30            match self.apply_single_fix(&mut fixed_content, issue, fix, &mut offset_adjustment) {
31                Ok(true) => self.fixes_applied += 1,
32                Ok(false) => {}, // Fix not applicable
33                Err(e) => eprintln!("Warning: Failed to apply fix for {}: {}", issue.rule, e),
34            }
35        }
36        
37        Ok(fixed_content)
38    }
39    
40    fn apply_single_fix(
41        &self,
42        content: &mut String,
43        issue: &Issue,
44        fix: &Fix,
45        offset_adjustment: &mut i64,
46    ) -> Result<bool, Box<dyn std::error::Error>> {
47        for replacement in &fix.replacements {
48            let start = (replacement.start as i64 + *offset_adjustment) as usize;
49            let end = (replacement.end as i64 + *offset_adjustment) as usize;
50            
51            if start > content.len() || end > content.len() || start > end {
52                return Ok(false); // Invalid range, skip this fix
53            }
54            
55            let original_len = end - start;
56            let new_len = replacement.text.len();
57            
58            content.replace_range(start..end, &replacement.text);
59            
60            // Update offset for subsequent fixes
61            *offset_adjustment += new_len as i64 - original_len as i64;
62        }
63        
64        Ok(true)
65    }
66}
67
68// Import reorganization functionality
69pub struct ImportOrganizer {
70    pub preserve_comments: bool,
71    pub group_external_crates: bool,
72    pub sort_within_groups: bool,
73}
74
75impl ImportOrganizer {
76    pub fn new() -> Self {
77        Self {
78            preserve_comments: true,
79            group_external_crates: true,
80            sort_within_groups: true,
81        }
82    }
83    
84    pub fn organize_imports(&self, content: &str) -> Result<String, Box<dyn std::error::Error>> {
85        let syntax_tree: SynFile = syn::parse_str(content)?;
86        
87        let mut use_items = Vec::new();
88        let mut non_use_items = Vec::new();
89        let mut use_positions = Vec::new();
90        
91        for (index, item) in syntax_tree.items.iter().enumerate() {
92            if let Item::Use(use_item) = item {
93                use_items.push(use_item.clone());
94                use_positions.push(index);
95            } else {
96                non_use_items.push(item.clone());
97            }
98        }
99        
100        if use_items.is_empty() {
101            return Ok(content.to_string());
102        }
103        
104        // Organize imports into groups
105        let organized_imports = self.group_and_sort_imports(use_items)?;
106        
107        // Reconstruct the file with organized imports
108        self.reconstruct_file_with_organized_imports(content, &organized_imports, &use_positions)
109    }
110    
111    fn group_and_sort_imports(&self, use_items: Vec<ItemUse>) -> Result<Vec<String>, Box<dyn std::error::Error>> {
112        let mut std_imports = Vec::new();
113        let mut external_imports = Vec::new();
114        let mut local_imports = Vec::new();
115        
116        for use_item in use_items {
117            let import_str = quote::quote!(#use_item).to_string();
118            let cleaned = import_str.replace(" ", "").replace("\n", "");
119            
120            if self.is_std_import(&use_item) {
121                std_imports.push(import_str);
122            } else if self.is_external_import(&use_item) {
123                external_imports.push(import_str);
124            } else {
125                local_imports.push(import_str);
126            }
127        }
128        
129        if self.sort_within_groups {
130            std_imports.sort();
131            external_imports.sort();
132            local_imports.sort();
133        }
134        
135        let mut organized = Vec::new();
136        
137        if !std_imports.is_empty() {
138            organized.extend(std_imports);
139            organized.push(String::new()); // Empty line separator
140        }
141        
142        if !external_imports.is_empty() {
143            organized.extend(external_imports);
144            organized.push(String::new()); // Empty line separator
145        }
146        
147        if !local_imports.is_empty() {
148            organized.extend(local_imports);
149        }
150        
151        // Remove trailing empty line
152        if organized.last() == Some(&String::new()) {
153            organized.pop();
154        }
155        
156        Ok(organized)
157    }
158    
159    fn is_std_import(&self, use_item: &ItemUse) -> bool {
160        let path_str = self.use_tree_to_string(&use_item.tree);
161        path_str.starts_with("std::") || 
162        path_str.starts_with("core::") || 
163        path_str.starts_with("alloc::")
164    }
165    
166    fn is_external_import(&self, use_item: &ItemUse) -> bool {
167        let path_str = self.use_tree_to_string(&use_item.tree);
168        // Simple heuristic: if it doesn't start with std/core/alloc and doesn't contain "::" in the first segment,
169        // it's likely an external crate
170        if self.is_std_import(use_item) {
171            return false;
172        }
173        
174        // Check if it starts with a simple identifier (external crate) vs a complex path (local module)
175        let first_segment = path_str.split("::").next().unwrap_or("");
176        !first_segment.starts_with("crate") && 
177        !first_segment.starts_with("super") && 
178        !first_segment.starts_with("self") &&
179        !path_str.starts_with("::")
180    }
181    
182    fn use_tree_to_string(&self, tree: &UseTree) -> String {
183        quote::quote!(#tree).to_string()
184    }
185    
186    fn reconstruct_file_with_organized_imports(
187        &self,
188        original_content: &str,
189        organized_imports: &[String],
190        use_positions: &[usize],
191    ) -> Result<String, Box<dyn std::error::Error>> {
192        let lines: Vec<&str> = original_content.lines().collect();
193        let mut result_lines = Vec::new();
194        let mut import_region_inserted = false;
195        let mut skip_import_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
196        
197        // Find lines that contain use statements to skip
198        let syntax_tree: SynFile = syn::parse_str(original_content)?;
199        
200        // For simplicity, we'll replace the first use statement with all organized imports
201        // and remove subsequent use statements
202        let mut first_use_line: Option<usize> = None;
203        
204        for line in &syntax_tree.items {
205            if let Item::Use(_) = line {
206                // Find the line number of this use statement
207                // This is simplified - in a real implementation, you'd need to track line numbers more carefully
208                break;
209            }
210        }
211        
212        // Reconstruct file with organized imports
213        let mut in_import_region = false;
214        for (line_idx, line) in lines.iter().enumerate() {
215            let line_trimmed = line.trim();
216            
217            // Detect import lines
218            if line_trimmed.starts_with("use ") && line_trimmed.ends_with(";") {
219                if !import_region_inserted {
220                    // Insert all organized imports here
221                    for import in organized_imports {
222                        if !import.is_empty() {
223                            result_lines.push(import.clone());
224                        } else {
225                            result_lines.push(String::new());
226                        }
227                    }
228                    import_region_inserted = true;
229                }
230                // Skip the original import line
231                continue;
232            }
233            
234            result_lines.push(line.to_string());
235        }
236        
237        Ok(result_lines.join("\n"))
238    }
239    
240    pub fn create_import_fix(&self, content: &str) -> Result<Option<Fix>, Box<dyn std::error::Error>> {
241        let organized = self.organize_imports(content)?;
242        
243        if organized == content {
244            return Ok(None); // No changes needed
245        }
246        
247        // Create a fix that replaces the entire content
248        // In a more sophisticated implementation, you'd calculate specific ranges
249        let fix = Fix {
250            description: "Reorganize imports".to_string(),
251            replacements: vec![Replacement {
252                start: 0,
253                end: content.len(),
254                text: organized,
255            }],
256        };
257        
258        Ok(Some(fix))
259    }
260}
261
262// Naming convention fixes
263pub struct NamingConventionFixer;
264
265impl NamingConventionFixer {
266    pub fn new() -> Self {
267        Self
268    }
269    
270    pub fn create_snake_case_fix(&self, identifier: &str, location: &Location) -> Option<Fix> {
271        let snake_case = self.to_snake_case(identifier);
272        
273        if snake_case == identifier {
274            return None;
275        }
276        
277        Some(Fix {
278            description: format!("Convert '{}' to snake_case: '{}'", identifier, snake_case),
279            replacements: vec![Replacement {
280                start: location.column.saturating_sub(1),
281                end: location.column + identifier.len() - 1,
282                text: snake_case,
283            }],
284        })
285    }
286    
287    pub fn create_pascal_case_fix(&self, identifier: &str, location: &Location) -> Option<Fix> {
288        let pascal_case = self.to_pascal_case(identifier);
289        
290        if pascal_case == identifier {
291            return None;
292        }
293        
294        Some(Fix {
295            description: format!("Convert '{}' to PascalCase: '{}'", identifier, pascal_case),
296            replacements: vec![Replacement {
297                start: location.column.saturating_sub(1),
298                end: location.column + identifier.len() - 1,
299                text: pascal_case,
300            }],
301        })
302    }
303    
304    fn to_snake_case(&self, s: &str) -> String {
305        let mut result = String::new();
306        let mut chars = s.chars().peekable();
307        
308        while let Some(ch) = chars.next() {
309            if ch.is_uppercase() {
310                if !result.is_empty() && !result.ends_with('_') {
311                    result.push('_');
312                }
313                result.push(ch.to_lowercase().next().unwrap_or(ch));
314            } else {
315                result.push(ch);
316            }
317        }
318        
319        result
320    }
321    
322    fn to_pascal_case(&self, s: &str) -> String {
323        let mut result = String::new();
324        let mut capitalize_next = true;
325        
326        for ch in s.chars() {
327            if ch == '_' {
328                capitalize_next = true;
329            } else if capitalize_next {
330                result.push(ch.to_uppercase().next().unwrap_or(ch));
331                capitalize_next = false;
332            } else {
333                result.push(ch);
334            }
335        }
336        
337        result
338    }
339}
340
341// Documentation template generator
342pub struct DocTemplateGenerator;
343
344impl DocTemplateGenerator {
345    pub fn new() -> Self {
346        Self
347    }
348    
349    pub fn generate_function_doc_fix(&self, fn_name: &str, location: &Location, has_params: bool, has_return: bool) -> Fix {
350        let mut doc_lines = vec![
351            "/// ".to_string(),
352            format!("/// {}", self.generate_function_description(fn_name)),
353        ];
354        
355        if has_params {
356            doc_lines.push("///".to_string());
357            doc_lines.push("/// # Arguments".to_string());
358            doc_lines.push("///".to_string());
359            doc_lines.push("/// * `param` - Description of the parameter".to_string());
360        }
361        
362        if has_return {
363            doc_lines.push("///".to_string());
364            doc_lines.push("/// # Returns".to_string());
365            doc_lines.push("///".to_string());
366            doc_lines.push("/// Description of the return value".to_string());
367        }
368        
369        doc_lines.push("///".to_string());
370        doc_lines.push("/// # Examples".to_string());
371        doc_lines.push("///".to_string());
372        doc_lines.push("/// ```".to_string());
373        doc_lines.push(format!("/// // Example usage of {}", fn_name));
374        doc_lines.push("/// ```".to_string());
375        
376        let doc_text = doc_lines.join("\n") + "\n";
377        
378        Fix {
379            description: format!("Add documentation template for function '{}'", fn_name),
380            replacements: vec![Replacement {
381                start: location.column.saturating_sub(1),
382                end: location.column.saturating_sub(1),
383                text: doc_text,
384            }],
385        }
386    }
387    
388    pub fn generate_struct_doc_fix(&self, struct_name: &str, location: &Location) -> Fix {
389        let doc_text = format!(
390            "/// {}\n///\n/// # Examples\n///\n/// ```\n/// // Example usage of {}\n/// ```\n",
391            self.generate_struct_description(struct_name),
392            struct_name
393        );
394        
395        Fix {
396            description: format!("Add documentation template for struct '{}'", struct_name),
397            replacements: vec![Replacement {
398                start: location.column.saturating_sub(1),
399                end: location.column.saturating_sub(1),
400                text: doc_text,
401            }],
402        }
403    }
404    
405    fn generate_function_description(&self, fn_name: &str) -> String {
406        // Simple heuristic to generate meaningful descriptions
407        if fn_name.starts_with("get_") {
408            format!("Gets the {}.", &fn_name[4..].replace('_', " "))
409        } else if fn_name.starts_with("set_") {
410            format!("Sets the {}.", &fn_name[4..].replace('_', " "))
411        } else if fn_name.starts_with("is_") {
412            format!("Checks if {}.", &fn_name[3..].replace('_', " "))
413        } else if fn_name.starts_with("has_") {
414            format!("Checks if has {}.", &fn_name[4..].replace('_', " "))
415        } else if fn_name.starts_with("create_") || fn_name.starts_with("new_") {
416            format!("Creates a new {}.", fn_name.replace('_', " "))
417        } else {
418            format!("TODO: Add description for {}.", fn_name)
419        }
420    }
421    
422    fn generate_struct_description(&self, struct_name: &str) -> String {
423        format!("Represents a {}.", struct_name.replace('_', " ").to_lowercase())
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    
431    #[test]
432    fn test_naming_convention_fixer() {
433        let fixer = NamingConventionFixer::new();
434        
435        assert_eq!(fixer.to_snake_case("CamelCase"), "camel_case");
436        assert_eq!(fixer.to_snake_case("XMLHttpRequest"), "x_m_l_http_request");
437        assert_eq!(fixer.to_pascal_case("snake_case"), "SnakeCase");
438        assert_eq!(fixer.to_pascal_case("already_pascal"), "AlreadyPascal");
439    }
440    
441    #[test]
442    fn test_import_organizer() {
443        let organizer = ImportOrganizer::new();
444        let content = r#"
445use std::collections::HashMap;
446use serde::Serialize;
447use crate::local_module;
448use std::fs;
449
450fn main() {}
451"#;
452        
453        let organized = organizer.organize_imports(content).unwrap();
454        assert!(organized.contains("use std::"));
455        assert!(organized.contains("use serde::"));
456        assert!(organized.contains("use crate::"));
457    }
458}