1
2#[derive(Debug, Clone)]
4pub struct Fix {
5 pub old: String,
6 pub new: String,
7 pub file: String,
8 pub line: usize,
9}
10
11pub fn extract_fixes(memory_content: &str) -> Vec<(String, String)> {
13 let mut fixes = Vec::new();
14
15 if let Ok(re) = regex_lite::Regex::new(r"(?m)'([^']+)'\s*(?:→|->|=>)\s*'([^']+)'") {
16 for cap in re.captures_iter(memory_content) {
17 fixes.push((cap[1].to_string(), cap[2].to_string()));
18 }
19 }
20
21 if let Ok(re) = regex_lite::Regex::new(r"(?m)`([^`]+)`\s*(?:→|->|=>)\s*`([^`]+)`") {
22 for cap in re.captures_iter(memory_content) {
23 fixes.push((cap[1].to_string(), cap[2].to_string()));
24 }
25 }
26
27 if let Ok(re) = regex_lite::Regex::new(r"(?i)change\s+'([^']+)'\s+to\s+'([^']+)'") {
28 for cap in re.captures_iter(memory_content) {
29 fixes.push((cap[1].to_string(), cap[2].to_string()));
30 }
31 }
32
33 if let Ok(re) = regex_lite::Regex::new(r"(?i)(?:replace|swap|switch)\s+'([^']+)'\s+(?:with|to|for)\s+'([^']+)'") {
34 for cap in re.captures_iter(memory_content) {
35 fixes.push((cap[1].to_string(), cap[2].to_string()));
36 }
37 }
38
39 if let Ok(re) = regex_lite::Regex::new(r"s/([^/]+)/([^/]*)/") {
40 for cap in re.captures_iter(memory_content) {
41 let old_str = cap[1].to_string();
42 let new_str = cap[2].to_string();
43 if !old_str.is_empty() && !new_str.is_empty() && old_str != new_str {
44 fixes.push((old_str, new_str));
45 }
46 }
47 }
48
49 if let Ok(re) = regex_lite::Regex::new(r"(?m)(\S+(?:\s+\S+){0,5})\s*(?:→|->|=>)\s*(\S+(?:\s+\S+){0,5})") {
50 for cap in re.captures_iter(memory_content) {
51 let old_str = cap[1].trim().to_string();
52 let new_str = cap[2].trim().to_string();
53 if old_str.len() >= 2 && new_str.len() >= 2 && old_str != new_str {
54 fixes.push((old_str, new_str));
55 }
56 }
57 }
58
59 fixes
60}
61
62pub fn find_function<'a>(lines: &[&'a str], signature: &str) -> Option<(usize, &'a str)> {
64 let trimmed = signature.trim();
65
66 for (i, l) in lines.iter().enumerate() {
67 if l.trim() == trimmed { return Some((i, l)); }
68 }
69
70 for (i, l) in lines.iter().enumerate() {
71 if l.trim().starts_with(trimmed) { return Some((i, l)); }
72 }
73
74 let func_name = extract_func_name(trimmed);
75 if func_name.len() >= 3 {
76 let keywords = ["fn ", "def ", "function ", "pub ", "struct ", "class ", "trait ", "impl ", "enum "];
77 for (i, l) in lines.iter().enumerate() {
78 let t = l.trim();
79 if t.contains(&func_name) && keywords.iter().any(|k| t.contains(k)) {
80 return Some((i, l));
81 }
82 }
83 for (i, l) in lines.iter().enumerate() {
84 if l.contains(&func_name) { return Some((i, l)); }
85 }
86 }
87
88 None
89}
90
91fn extract_func_name(sig: &str) -> String {
92 let skip = ["def", "fn", "function", "pub", "private", "protected", "static", "async",
93 "export", "default", "const", "let", "var", "class", "struct", "enum", "trait",
94 "impl", "interface", "type"];
95 sig.split(['(', ' ', '{'])
96 .find(|s| {
97 let t = s.trim();
98 !t.is_empty() && t.chars().all(|c| c.is_alphanumeric() || c == '_')
99 && !skip.contains(&t)
100 })
101 .unwrap_or("")
102 .to_string()
103}
104
105pub fn find_boundary(lines: &[&str], start: usize, base_indent: usize) -> usize {
107 if start >= lines.len() { return lines.len().saturating_sub(1); }
108 for i in start..lines.len() {
109 let line = lines[i];
110 if line.trim().is_empty() || line.trim().starts_with('#') || line.trim().starts_with("//") {
111 continue;
112 }
113 let indent = line.len() - line.trim_start().len();
114 if indent <= base_indent {
115 let mut j = i;
116 while j > start && (lines[j - 1].trim().is_empty()
117 || lines[j - 1].trim().starts_with('#')
118 || lines[j - 1].trim().starts_with("//"))
119 { j -= 1; }
120 if j > start { return j - 1; }
121 return i.saturating_sub(1);
122 }
123 }
124 lines.len().saturating_sub(1)
125}
126
127pub fn apply_fixes(content: &str, fixes: &[(String, String)]) -> (String, usize) {
129 let mut modified = content.to_string();
130 let mut total = 0;
131 for (old, new) in fixes {
132 if old == new { continue; }
133 let count = modified.matches(old).count();
134 if count > 0 {
135 modified = modified.replace(old, new);
136 total += count;
137 } else {
138 let unquoted = old.trim_matches(|c| c == '\'' || c == '"' || c == '`');
139 if unquoted.len() < old.len() {
140 let count2 = modified.matches(unquoted).count();
141 if count2 > 0 {
142 modified = modified.replace(unquoted, new);
143 total += count2;
144 }
145 }
146 }
147 }
148 (modified, total)
149}
150
151pub fn content_aware_match(memory_content: &str, file_content: &str) -> Vec<(String, String)> {
153 let mut results = Vec::new();
154 if let Ok(re) = regex_lite::Regex::new(r"['`]?(\S{2,})['`]?\s*(?:→|->|=>)\s*['`]?(\S{2,})['`]?") {
155 for cap in re.captures_iter(memory_content) {
156 let old_str = cap[1].trim_matches(|c| c == '\'' || c == '"' || c == '`').to_string();
157 let new_str = cap[2].trim_matches(|c| c == '\'' || c == '"' || c == '`').to_string();
158 if file_content.contains(&old_str) && old_str.len() >= 2 {
159 results.push((old_str, new_str));
160 }
161 }
162 }
163 results
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_extract_quoted_fix() {
172 let fixes = extract_fixes("'if x < 2' → 'if x == 2'");
173 assert!(!fixes.is_empty());
174 assert_eq!(fixes[0].0, "if x < 2");
175 }
176
177 #[test]
178 fn test_extract_change_pattern() {
179 let fixes = extract_fixes("change 'old_val' to 'new_val'");
180 assert!(!fixes.is_empty());
181 assert_eq!(fixes[0].0, "old_val");
182 }
183
184 #[test]
185 fn test_find_function_exact() {
186 let lines = vec!["pub fn foo() {}", "fn bar(x: i32) {}", "fn baz() {}"];
187 let (idx, _) = find_function(&lines, "fn bar(x: i32)").unwrap();
188 assert_eq!(idx, 1);
189 }
190
191 #[test]
192 fn test_find_function_fuzzy() {
193 let lines = vec!["pub fn process_data(config: Config) -> Result", "fn helper() {}"];
194 let (idx, _) = find_function(&lines, "process_data").unwrap();
195 assert_eq!(idx, 0);
196 }
197
198 #[test]
199 fn test_find_boundary() {
200 let lines = vec!["fn foo() {", " let x = 1;", "}", "fn bar() {}"];
201 let end = find_boundary(&lines, 1, 0);
205 assert_eq!(end, 1); }
207
208 #[test]
209 fn test_apply_replace() {
210 let fixes = vec![("old_thing".to_string(), "new_thing".to_string())];
211 let (result, count) = apply_fixes("use old_thing;", &fixes);
212 assert_eq!(count, 1);
213 assert!(!result.contains("old_thing"));
214 assert!(result.contains("new_thing"));
215 }
216
217 #[test]
218 fn test_content_aware_match() {
219 let mem = "'func_a' → 'func_b'";
220 let content = "use func_a;";
221 let matches = content_aware_match(mem, content);
222 assert!(!matches.is_empty(), "should find match for func_a");
223 assert_eq!(matches[0].0, "func_a");
224 }
225}