Skip to main content

reliary_fix/
lib.rs

1
2/// A resolved fix: old text → new text, applied to a specific file
3#[derive(Debug, Clone)]
4pub struct Fix {
5    pub old: String,
6    pub new: String,
7    pub file: String,
8    pub line: usize,
9}
10
11/// Pattern extraction from memory content supporting multiple formats.
12pub 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
62/// Forgiving signature matching: find a function by fuzzy signature.
63pub 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
105/// Grammar-free boundary detection: find end of function by indentation
106pub 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
127/// Apply fixes to file content (in-memory)
128pub 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
151/// Content-aware matching: find old/new pairs where old exists in content
152pub 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        // start=1, base_indent=0
202        // i=1: indent 4 > 0, continue
203        // i=2: indent 0 <= 0, j=2, lines[1] not blank/comment, j>start → return j-1=1
204        let end = find_boundary(&lines, 1, 0);
205        assert_eq!(end, 1); // last content line before next function
206    }
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}