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 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 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) => {}, 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); }
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 *offset_adjustment += new_len as i64 - original_len as i64;
62 }
63
64 Ok(true)
65 }
66}
67
68pub 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 let organized_imports = self.group_and_sort_imports(use_items)?;
106
107 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()); }
141
142 if !external_imports.is_empty() {
143 organized.extend(external_imports);
144 organized.push(String::new()); }
146
147 if !local_imports.is_empty() {
148 organized.extend(local_imports);
149 }
150
151 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 if self.is_std_import(use_item) {
171 return false;
172 }
173
174 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 let syntax_tree: SynFile = syn::parse_str(original_content)?;
199
200 let mut first_use_line: Option<usize> = None;
203
204 for line in &syntax_tree.items {
205 if let Item::Use(_) = line {
206 break;
209 }
210 }
211
212 let mut in_import_region = false;
214 for (line_idx, line) in lines.iter().enumerate() {
215 let line_trimmed = line.trim();
216
217 if line_trimmed.starts_with("use ") && line_trimmed.ends_with(";") {
219 if !import_region_inserted {
220 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 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); }
246
247 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
262pub 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
341pub 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 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}