Skip to main content

reliary_fix/
lib.rs

1#![forbid(unsafe_code)]
2use std::sync::LazyLock;
3
4/// Cached fix-extraction regexes. Compiled once on first use.
5static 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
20/// Pattern extraction from memory content supporting multiple formats.
21pub 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    // D5: Borrow keys from captures, only own at final push.
26    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
58/// Forgiving signature matching: find a function by fuzzy signature.
59pub 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        // D15: Sorted array + binary search (small N, but cleaner).
73        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    // D15: Sorted for binary search future.
90    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
103/// Grammar-free boundary detection: find end of function by indentation
104pub 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        // D14: Cache trim once per line.
109        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
131/// Apply fixes to file content (in-memory).
132/// D9: AhoCorasick single-pass replace instead of per-fix O(content) replace.
133/// D10: Single count-and-replace pass.
134/// D13: Empty fixes guard.
135pub fn apply_fixes(content: &str, fixes: &[(String, String)]) -> (String, usize) {
136    if fixes.is_empty() {
137        return (content.to_string(), 0);
138    }
139    // Collect all unique old→new pairs for AhoCorasick.
140    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
169/// Content-aware matching: find old/new pairs where old exists in content
170pub 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        // start=1, base_indent=0
218        // i=1: indent 4 > 0, continue
219        // i=2: indent 0 <= 0, j=2, lines[1] not blank/comment, j>start → return j-1=1
220        let end = find_boundary(&lines, 1, 0);
221        assert_eq!(end, 1); // last content line before next function
222    }
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}