use std::path::{Path, PathBuf};
use anyhow::Context;
use crate::ast::rewrite::{FunctionSigEdit, rewrite_function_signature};
use crate::ast::{Language, rename, replace as ast_replace};
use crate::containment::PathGuard;
use crate::fallback::{EditError, EditErrorKind};
use crate::plan::Operation;
use crate::write::WritePolicy;
use super::{
ApplyMode, ContentEditResult, EditResult, ensure_contained, make_diff, write_if_apply,
};
#[cfg(all(feature = "ast", any(feature = "cli", feature = "files")))]
pub fn ast_rewrite_signature(
path: &Path,
name: &str,
edit: &FunctionSigEdit,
new_signature: Option<&str>,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
if new_signature.is_none()
&& edit.visibility.is_none()
&& edit.parameters.is_none()
&& edit.return_type.is_none()
{
return Err(EditError::new(
EditErrorKind::InvalidInput,
"ast_rewrite_signature requires new_signature and/or visibility/parameters/return_type",
)
.into());
}
let op = Operation::AstRewriteSignature {
path: path.to_string_lossy().into(),
old: name.into(),
new_signature: new_signature.map(str::to_string),
visibility: edit.visibility.clone(),
parameters: edit.parameters.clone(),
return_type: edit.return_type.clone(),
lang: None,
};
let cwd = path.parent().unwrap_or_else(|| Path::new("."));
super::execute_as_edit_result(op, mode, cwd, guard, "ast.rewrite_signature", None)
}
#[cfg(feature = "ast")]
pub fn ast_rewrite_signature_in_content(
content: &str,
function_name: &str,
edit: &FunctionSigEdit,
new_signature: Option<&str>,
lang: Language,
) -> anyhow::Result<ContentEditResult> {
let new_content = if let Some(full) = new_signature {
let _ = lang;
crate::ast::rewrite::replace_function_signature(content, function_name, full).ok_or_else(
|| {
EditError::new(
EditErrorKind::NoMatch,
format!("function `{function_name}` not found for signature rewrite"),
)
},
)?
} else {
rewrite_function_signature(content, function_name, edit, lang).ok_or_else(|| {
EditError::new(
EditErrorKind::NoMatch,
format!("function `{function_name}` not found for signature rewrite"),
)
})?
};
let changed = new_content != content;
let diff = if changed {
make_diff("<content>", content, &new_content)
} else {
String::new()
};
Ok(ContentEditResult {
original: content.to_string(),
new_content,
diff,
changed,
match_count: if changed { 1 } else { 0 },
})
}
#[cfg(all(feature = "ast", any(feature = "cli", feature = "files")))]
pub fn ast_rename(
path: &Path,
old: &str,
new: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
if old.is_empty() || new.is_empty() {
return Err(EditError::new(
EditErrorKind::InvalidInput,
"ast_rename old/new must not be empty",
)
.into());
}
ensure_contained(guard, path)?;
let path_str = path.to_string_lossy().into_owned();
let original = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {path_str}"))
.map_err(|e| EditError::new(EditErrorKind::OperationFailed, e.to_string()))?;
let lang = Language::from_path(path);
let Some(renamed) = rename::rename_in_source(&original, old, new, lang) else {
return Err(EditError::new(
EditErrorKind::ParseError,
format!("failed to parse {} as {:?}", path_str, lang),
)
.into());
};
if renamed.replacements == 0 {
return Err(EditError::new(
EditErrorKind::NoMatch,
format!("no identifier matches for `{old}` in {path_str}"),
)
.into());
}
let policy = WritePolicy::default();
let applied = write_if_apply(path, &renamed.content, mode, &policy, guard)?;
let mut result = super::build_edit_result(
&path_str,
original,
renamed.content,
applied,
"ast.rename",
None,
);
result.match_count = renamed.replacements;
Ok(result)
}
#[cfg(all(feature = "ast", any(feature = "cli", feature = "files")))]
pub fn ast_replace_in_symbol(
path: &Path,
symbol: &str,
old: &str,
new: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
ensure_contained(guard, path)?;
let path_str = path.to_string_lossy().into_owned();
let original =
std::fs::read_to_string(path).with_context(|| format!("failed to read {path_str}"))?;
let lang = Language::from_path(path);
let replaced = match ast_replace::replace_in_symbol(&original, symbol, old, new, false, lang) {
Ok(Some(r)) => r,
Ok(None) => {
return Err(EditError::new(
EditErrorKind::NoMatch,
format!("symbol `{symbol}` not found in {path_str}"),
)
.into());
}
Err(e) => {
return Err(EditError::new(EditErrorKind::OperationFailed, e.to_string()).into());
}
};
if replaced.replacements == 0 {
return Err(EditError::new(
EditErrorKind::NoMatch,
format!("no matches for `{old}` in symbol `{symbol}` in {path_str}"),
)
.into());
}
let policy = WritePolicy::default();
let applied = write_if_apply(path, &replaced.content, mode, &policy, guard)?;
let mut result = super::build_edit_result(
&path_str,
original,
replaced.content,
applied,
"ast.replace_in_symbol",
None,
);
result.match_count = replaced.replacements;
Ok(result)
}
#[derive(Debug, Clone)]
pub struct AstRenameBatchOptions {
pub mode: ApplyMode,
pub continue_on_no_match: bool,
pub fail_fast: bool,
}
impl Default for AstRenameBatchOptions {
fn default() -> Self {
Self {
mode: ApplyMode::Preview,
continue_on_no_match: true,
fail_fast: false,
}
}
}
#[derive(Debug)]
pub struct AstRenameFileResult {
pub path: PathBuf,
pub result: Result<EditResult, EditError>,
}
#[cfg(all(feature = "ast", any(feature = "cli", feature = "files")))]
pub fn ast_rename_batch(
paths: &[&Path],
old: &str,
new: &str,
opts: &AstRenameBatchOptions,
guard: Option<&PathGuard>,
) -> anyhow::Result<Vec<AstRenameFileResult>> {
if old.is_empty() || new.is_empty() {
return Err(EditError::new(
EditErrorKind::InvalidInput,
"ast_rename_batch old/new must not be empty",
)
.into());
}
let mut seen = std::collections::HashSet::new();
let mut unique_paths: Vec<&Path> = Vec::new();
for p in paths {
let key = p.to_path_buf();
if seen.insert(key) {
unique_paths.push(*p);
}
}
let mut out = Vec::with_capacity(unique_paths.len());
for path in unique_paths {
match ast_rename(path, old, new, opts.mode, guard) {
Ok(r) => out.push(AstRenameFileResult {
path: path.to_path_buf(),
result: Ok(r),
}),
Err(e) => {
let edit_err = e.downcast::<EditError>().unwrap_or_else(|e| {
EditError::new(EditErrorKind::OperationFailed, e.to_string())
});
let is_no_match = edit_err.kind == EditErrorKind::NoMatch;
out.push(AstRenameFileResult {
path: path.to_path_buf(),
result: Err(edit_err),
});
if is_no_match && !opts.continue_on_no_match {
break;
}
if !is_no_match && opts.fail_fast {
break;
}
}
}
}
Ok(out)
}