use super::common::{resolve_lang, setup_multi_file};
use crate::cli::global::GlobalFlags;
use crate::cmd::output::{WritePhase, run_write_op, stage_for_write};
use crate::cmd::write_mode::{RenderPolicy, WriteMessages, finalize_execution_result};
use crate::exit;
use crate::plan::Operation;
use crate::tx::engine::WriteSource;
use anyhow::Context;
use clap::Args;
use serde::Serialize;
#[derive(Debug, Args)]
pub struct RenameArgs {
pub path: String,
#[arg(long)]
pub old: String,
#[arg(long)]
pub new: String,
#[arg(long)]
pub lang: Option<String>,
#[command(flatten)]
pub write: crate::cli::global::WriteFlags,
}
pub(super) fn run_rename(args: RenameArgs, global: &GlobalFlags) -> anyhow::Result<u8> {
let (cwd, paths) = setup_multi_file(&args.path, global)?;
crate::verbose!(
"ast rename: '{}' -> '{}' in {}",
args.old,
args.new,
args.path
);
crate::verbose!("ast rename: scanning {} files", paths.len());
let mut operations = Vec::new();
let lang_hint = args.lang.as_deref();
for path in &paths {
let lang = resolve_lang(lang_hint, path);
let source =
std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
let has_match = if lang.has_grammar() {
crate::ast::rename::rename_in_source(&source, &args.old, &args.new, lang)
.is_some_and(|r| r.replacements > 0)
} else {
false
} || {
crate::ops::replace::compile_replace_regex(&args.old, false, false, false, true)
.ok()
.flatten()
.is_some_and(|re| re.is_match(&source))
};
if has_match {
let rel = path
.strip_prefix(&cwd)
.unwrap_or(path)
.to_string_lossy()
.into_owned();
operations.push(Operation::AstRename {
path: rel,
old: args.old.clone(),
new: args.new.clone(),
lang: args.lang.clone(),
});
}
}
if operations.is_empty() {
let msg = format!("symbol '{}' not found in {}", args.old, args.path);
global.emit_error_json(&msg)?;
return Ok(exit::NO_MATCHES);
}
let files_changed = operations.len();
let (cwd, result) = match stage_for_write(WriteSource::Operations(operations), global) {
Ok(v) => v,
Err(e) if exit::is_no_match(&e) => {
let msg = e.to_string();
global.emit_error_json(&msg)?;
return Ok(exit::NO_MATCHES);
}
Err(e) => return Err(e),
};
#[derive(serde::Serialize)]
struct RenameOut {
ok: bool,
files_changed: usize,
}
let check_msg = format!("would rename in {files_changed} file(s)");
let apply_msg = format!("renamed in {files_changed} file(s)");
finalize_execution_result(
global,
&cwd,
result,
|_phase, _diff| RenameOut {
ok: true,
files_changed,
},
WriteMessages {
check: &check_msg,
apply: &apply_msg,
post_confirm: None,
},
RenderPolicy::default(),
)
}
#[derive(Debug, Args)]
pub struct ReplaceArgs {
pub path: String,
pub symbol: String,
#[arg(long)]
pub old: String,
#[arg(long = "new")]
pub new_text: String,
#[arg(long)]
pub regex: bool,
#[arg(long)]
pub lang: Option<String>,
#[command(flatten)]
pub write: crate::cli::global::WriteFlags,
}
#[derive(Debug, Serialize)]
struct AstReplaceOutput {
ok: bool,
symbol: String,
#[serde(skip_serializing_if = "Option::is_none")]
replacements: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
diff: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
applied: Option<bool>,
}
pub(super) fn run_replace(args: ReplaceArgs, global: &GlobalFlags) -> anyhow::Result<u8> {
crate::verbose!(
"ast replace: symbol={}, from={}, regex={}",
args.symbol,
args.old,
args.regex
);
let op = Operation::AstReplace {
path: args.path.clone(),
symbol: args.symbol.clone(),
old: args.old.clone(),
new_text: args.new_text.clone(),
regex: args.regex,
lang: args.lang.clone(),
};
let symbol = args.symbol.clone();
let check_msg = format!("would replace in symbol '{symbol}' in {}", args.path);
let apply_msg = format!("replaced in symbol '{symbol}' in {}", args.path);
match run_write_op(
op,
global,
|phase, diff| AstReplaceOutput {
ok: true,
symbol: symbol.clone(),
replacements: None, diff,
applied: match phase {
WritePhase::Confirmed(a) => Some(a),
_ => None,
},
},
&check_msg,
&apply_msg,
) {
Ok(code) => Ok(code),
Err(e) => {
if exit::is_no_match(&e) {
global.emit_error_json(&e.to_string())?;
Ok(exit::NO_MATCHES)
} else {
Err(e)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::global::GlobalFlags;
use std::fs;
use tempfile::TempDir;
#[test]
fn rename_path_first_with_old_new_flags_applies() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("mod.rs"), "fn alpha() {}\n").unwrap();
let mut global = GlobalFlags::test_with_cwd(dir.path());
global.apply = true;
let code = run_rename(
RenameArgs {
path: "mod.rs".into(),
old: "alpha".into(),
new: "beta".into(),
lang: None,
write: Default::default(),
},
&global,
)
.unwrap();
assert_eq!(code, exit::SUCCESS);
let content = fs::read_to_string(dir.path().join("mod.rs")).unwrap();
assert!(
content.contains("fn beta()"),
"expected rename, got: {content}"
);
assert!(!content.contains("fn alpha()"), "old name should be gone");
}
}