1#![forbid(unsafe_code)]
2use std::sync::LazyLock;
3
4static FIX_RE: LazyLock<[regex_lite::Regex; 6]> = LazyLock::new(|| {
6 [
7 regex_lite::Regex::new(r"(?m)'([^']+)'\s*(?:→|->|=>)\s*'([^']+)'").unwrap(),
8 regex_lite::Regex::new(r"(?m)`([^`]+)`\s*(?:→|->|=>)\s*`([^`]+)`").unwrap(),
9 regex_lite::Regex::new(r"(?i)change\s+'([^']+)'\s+to\s+'([^']+)'").unwrap(),
10 regex_lite::Regex::new(r"(?i)(?:replace|swap|switch)\s+'([^']+)'\s+(?:with|to|for)\s+'([^']+)'").unwrap(),
11 regex_lite::Regex::new(r"s/([^/]+)/([^/]*)/").unwrap(),
12 regex_lite::Regex::new(r"(?m)(\S+(?:\s+\S+){0,5})\s*(?:→|->|=>)\s*(\S+(?:\s+\S+){0,5})").unwrap(),
13 ]
14});
15
16static CAM_RE: LazyLock<regex_lite::Regex> = LazyLock::new(|| {
17 regex_lite::Regex::new(r"['`]?(\S{2,})['`]?\s*(?:→|->|=>)\s*['`]?(\S{2,})['`]?").unwrap()
18});
19
20pub fn extract_fixes(memory_content: &str) -> Vec<(String, String)> {
22 let mut fixes = Vec::with_capacity(8);
23 let mut seen: rustc_hash::FxHashSet<(String, String)> = rustc_hash::FxHashSet::default();
24
25 let push = |old: &str, new: &str, fixes: &mut Vec<(String, String)>, seen: &mut rustc_hash::FxHashSet<(String, String)>| {
27 if !old.is_empty() && old != new && seen.insert((old.to_string(), new.to_string())) {
28 fixes.push((old.to_string(), new.to_string()));
29 }
30 };
31
32 for cap in FIX_RE[0].captures_iter(memory_content) {
33 push(&cap[1], &cap[2], &mut fixes, &mut seen);
34 }
35 for cap in FIX_RE[1].captures_iter(memory_content) {
36 push(&cap[1], &cap[2], &mut fixes, &mut seen);
37 }
38 for cap in FIX_RE[2].captures_iter(memory_content) {
39 push(&cap[1], &cap[2], &mut fixes, &mut seen);
40 }
41 for cap in FIX_RE[3].captures_iter(memory_content) {
42 push(&cap[1], &cap[2], &mut fixes, &mut seen);
43 }
44 for cap in FIX_RE[4].captures_iter(memory_content) {
45 push(&cap[1], &cap[2], &mut fixes, &mut seen);
46 }
47 for cap in FIX_RE[5].captures_iter(memory_content) {
48 let old_str = cap[1].trim();
49 let new_str = cap[2].trim();
50 if old_str.len() >= 2 && new_str.len() >= 2 {
51 push(old_str, new_str, &mut fixes, &mut seen);
52 }
53 }
54
55 fixes
56}
57
58pub fn find_function<'a>(lines: &[&'a str], signature: &str) -> Option<(usize, &'a str)> {
60 let trimmed = signature.trim();
61
62 for (i, l) in lines.iter().enumerate() {
63 if l.trim() == trimmed { return Some((i, l)); }
64 }
65
66 for (i, l) in lines.iter().enumerate() {
67 if l.trim().starts_with(trimmed) { return Some((i, l)); }
68 }
69
70 let func_name = extract_func_name(trimmed);
71 if func_name.len() >= 3 {
72 const KEYWORDS: &[&str] = &["fn ", "def ", "function ", "pub ", "struct ", "class ", "trait ", "impl ", "enum "];
74 for (i, l) in lines.iter().enumerate() {
75 let t = l.trim();
76 if t.contains(&func_name) && KEYWORDS.iter().any(|k| t.contains(k)) {
77 return Some((i, l));
78 }
79 }
80 for (i, l) in lines.iter().enumerate() {
81 if l.contains(&func_name) { return Some((i, l)); }
82 }
83 }
84
85 None
86}
87
88fn extract_func_name(sig: &str) -> String {
89 const SKIP: &[&str] = &["async", "class", "const", "def", "default", "enum", "export",
91 "fn", "function", "impl", "interface", "let", "private", "protected", "pub",
92 "static", "struct", "trait", "type", "var"];
93 sig.split(['(', ' ', '{'])
94 .find(|s| {
95 let t = s.trim();
96 !t.is_empty() && t.chars().all(|c| c.is_alphanumeric() || c == '_')
97 && !SKIP.contains(&t)
98 })
99 .unwrap_or("")
100 .to_string()
101}
102
103pub fn find_boundary(lines: &[&str], start: usize, base_indent: usize) -> usize {
105 if start >= lines.len() { return lines.len().saturating_sub(1); }
106 for i in start..lines.len() {
107 let line = lines[i];
108 let trimmed = line.trim();
110 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.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 {
117 let prev_trimmed = lines[j - 1].trim();
118 if !(prev_trimmed.is_empty()
119 || prev_trimmed.starts_with('#')
120 || prev_trimmed.starts_with("//"))
121 { break; }
122 j -= 1;
123 }
124 if j > start { return j - 1; }
125 return i.saturating_sub(1);
126 }
127 }
128 lines.len().saturating_sub(1)
129}
130
131pub fn apply_fixes(content: &str, fixes: &[(String, String)]) -> (String, usize) {
136 if fixes.is_empty() {
137 return (content.to_string(), 0);
138 }
139 let mut unique_old: Vec<&str> = Vec::with_capacity(fixes.len());
141 for (old, _) in fixes {
142 if !old.is_empty() && !unique_old.contains(&old.as_str()) {
143 unique_old.push(old.as_str());
144 }
145 }
146 let Ok(ac) = aho_corasick::AhoCorasick::builder()
147 .match_kind(aho_corasick::MatchKind::LeftmostLongest)
148 .build(&unique_old) else {
149 return (content.to_string(), 0);
150 };
151 let mut result = String::with_capacity(content.len());
152 let mut last_end = 0;
153 let mut total = 0usize;
154 for mat in ac.find_iter(content) {
155 result.push_str(&content[last_end..mat.start()]);
156 let old = &unique_old[mat.pattern().as_usize()];
157 if let Some((_, new)) = fixes.iter().find(|(o, _)| o.as_str() == *old) {
158 result.push_str(new);
159 total += 1;
160 } else {
161 result.push_str(old);
162 }
163 last_end = mat.end();
164 }
165 result.push_str(&content[last_end..]);
166 (result, total)
167}
168
169pub fn content_aware_match(memory_content: &str, file_content: &str) -> Vec<(String, String)> {
171 let mut results = Vec::new();
172 for cap in CAM_RE.captures_iter(memory_content) {
173 let old_str = cap[1].trim_matches(|c| c == '\'' || c == '"' || c == '`').to_string();
174 let new_str = cap[2].trim_matches(|c| c == '\'' || c == '"' || c == '`').to_string();
175 if file_content.contains(&old_str) && old_str.len() >= 2 {
176 results.push((old_str, new_str));
177 }
178 }
179 results
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_extract_quoted_fix() {
188 let fixes = extract_fixes("'if x < 2' → 'if x == 2'");
189 assert!(!fixes.is_empty());
190 assert_eq!(fixes[0].0, "if x < 2");
191 }
192
193 #[test]
194 fn test_extract_change_pattern() {
195 let fixes = extract_fixes("change 'old_val' to 'new_val'");
196 assert!(!fixes.is_empty());
197 assert_eq!(fixes[0].0, "old_val");
198 }
199
200 #[test]
201 fn test_find_function_exact() {
202 let lines = vec!["pub fn foo() {}", "fn bar(x: i32) {}", "fn baz() {}"];
203 let (idx, _) = find_function(&lines, "fn bar(x: i32)").unwrap();
204 assert_eq!(idx, 1);
205 }
206
207 #[test]
208 fn test_find_function_fuzzy() {
209 let lines = vec!["pub fn process_data(config: Config) -> Result", "fn helper() {}"];
210 let (idx, _) = find_function(&lines, "process_data").unwrap();
211 assert_eq!(idx, 0);
212 }
213
214 #[test]
215 fn test_find_boundary() {
216 let lines = vec!["fn foo() {", " let x = 1;", "}", "fn bar() {}"];
217 let end = find_boundary(&lines, 1, 0);
221 assert_eq!(end, 1); }
223
224 #[test]
225 fn test_apply_replace() {
226 let fixes = vec![("old_thing".to_string(), "new_thing".to_string())];
227 let (result, count) = apply_fixes("use old_thing;", &fixes);
228 assert_eq!(count, 1);
229 assert!(!result.contains("old_thing"));
230 assert!(result.contains("new_thing"));
231 }
232
233 #[test]
234 fn test_content_aware_match() {
235 let mem = "'func_a' → 'func_b'";
236 let content = "use func_a;";
237 let matches = content_aware_match(mem, content);
238 assert!(!matches.is_empty(), "should find match for func_a");
239 assert_eq!(matches[0].0, "func_a");
240 }
241}