Skip to main content

recast_core/
rewrite.rs

1//! Per-file rewrite engine and unified-diff renderer.
2//!
3//! [`rewrite_text`] runs the compiled regex over a single in-memory
4//! string and produces a [`RewriteOutcome`] with the match count and
5//! the post-image. [`unified_diff`] turns a before/after pair into a
6//! standard unified-diff string via the `similar` crate.
7//! [`label_for_path`] cleans `./`-prefixed paths so diff headers stay
8//! readable when the user passes `.` as the root.
9
10use std::path::{Component, Path, PathBuf};
11
12use similar::TextDiff;
13
14#[cfg(feature = "script")]
15use crate::error::Result;
16use crate::pattern::CompiledPattern;
17#[cfg(feature = "script")]
18use crate::script::ScriptRewriter;
19
20/// Post-image of a single-file rewrite plus the match count that produced
21/// it. The pre-image stays with the caller — that's why it isn't carried
22/// here.
23#[derive(Debug, Clone)]
24pub struct RewriteOutcome {
25    pub after: String,
26    pub matches: usize,
27}
28
29/// Apply `pattern` to `before` and return the rewrite outcome. Counts
30/// matches and produces the new text in a single pass via
31/// `regex::replace_all` with an `expand`-driven closure.
32pub fn rewrite_text(pattern: &CompiledPattern, before: &str) -> RewriteOutcome {
33    let regex = pattern.regex();
34    let template = pattern.replacement();
35    let mut matches = 0usize;
36    let after = regex
37        .replace_all(before, |caps: &regex::Captures<'_>| {
38            matches += 1;
39            let mut dst = String::new();
40            caps.expand(template, &mut dst);
41            dst
42        })
43        .into_owned();
44    RewriteOutcome { after, matches }
45}
46
47/// Apply `pattern` to `before`, calling `script` once per match. The
48/// script's return value replaces each occurrence. Script errors abort
49/// the whole rewrite.
50#[cfg(feature = "script")]
51pub fn rewrite_text_scripted(
52    pattern: &CompiledPattern,
53    script: &ScriptRewriter,
54    before: &str,
55) -> Result<RewriteOutcome> {
56    use std::cell::RefCell;
57
58    let regex = pattern.regex();
59    let err_slot: RefCell<Option<crate::error::Error>> = RefCell::new(None);
60    let mut matches = 0usize;
61
62    let after = regex.replace_all(before, |caps: &regex::Captures<'_>| {
63        if err_slot.borrow().is_some() {
64            return String::new();
65        }
66        matches += 1;
67        let caps_vec: Vec<&str> =
68            caps.iter().map(|m| m.map(|m| m.as_str()).unwrap_or("")).collect();
69        match script.replace(&caps_vec) {
70            Ok(s) => s,
71            Err(e) => {
72                *err_slot.borrow_mut() = Some(e);
73                String::new()
74            }
75        }
76    });
77
78    if let Some(e) = err_slot.into_inner() {
79        return Err(e);
80    }
81    Ok(RewriteOutcome { after: after.into_owned(), matches })
82}
83
84/// Drop leading `./` (and repeats thereof) from a path so unified-diff
85/// headers read `a/src/a.rs` instead of `a/./src/a.rs`. Absolute paths
86/// and plain relative paths pass through unchanged.
87pub fn label_for_path(path: &Path) -> String {
88    // Fast path: most paths the planner labels are either absolute
89    // (`/.../file`) or plain relative (`src/file`); only paths the user
90    // wrote with a literal `.` prefix need the component walk +
91    // PathBuf rebuild. Empty input drops through to the slow path so
92    // the final-empty check returns `"."` as before.
93    match path.components().next() {
94        None | Some(Component::CurDir) => {}
95        _ => return path.to_string_lossy().into_owned(),
96    }
97    let mut buf = PathBuf::new();
98    let mut leading = true;
99    for c in path.components() {
100        if leading && matches!(c, Component::CurDir) {
101            continue;
102        }
103        leading = false;
104        buf.push(c.as_os_str());
105    }
106    if buf.as_os_str().is_empty() { ".".to_owned() } else { buf.to_string_lossy().into_owned() }
107}
108
109/// Render a unified diff between `before` and `after` with three lines
110/// of context, using `label` for the `a/`+`b/` header paths.
111pub fn unified_diff(label: &str, before: &str, after: &str) -> String {
112    let diff = TextDiff::from_lines(before, after);
113    let mut out = diff
114        .unified_diff()
115        .context_radius(3)
116        .header(&format!("a/{label}"), &format!("b/{label}"))
117        .to_string();
118    if !out.ends_with('\n') {
119        out.push('\n');
120    }
121    out
122}
123
124#[cfg(test)]
125#[path = "rewrite_tests.rs"]
126mod tests;