pub mod actions;
pub mod add_parameter;
pub mod extract_function;
pub mod inline_function;
pub mod inline_variable;
pub mod introduce_variable;
pub mod move_item;
pub mod rename;
use std::path::PathBuf;
use normalize_shadow::{EditInfo, Shadow};
pub struct PlannedEdit {
pub file: PathBuf,
pub original: String,
pub new_content: String,
pub description: String,
}
pub struct RefactoringPlan {
pub operation: String,
pub edits: Vec<PlannedEdit>,
pub warnings: Vec<String>,
}
pub struct RefactoringContext {
pub root: PathBuf,
pub editor: normalize_edit::Editor,
pub index: Option<normalize_facts::FileIndex>,
pub loader: normalize_languages::GrammarLoader,
}
pub struct References {
pub callers: Vec<CallerRef>,
pub importers: Vec<ImportRef>,
}
pub struct CallerRef {
pub file: String,
pub caller: String,
pub line: usize,
#[allow(dead_code)]
pub access: Option<String>,
pub confidence: &'static str,
}
pub struct ImportRef {
pub file: String,
pub name: String,
#[allow(dead_code)]
pub alias: Option<String>,
pub line: usize,
pub confidence: &'static str,
}
pub struct RefactoringExecutor {
pub root: PathBuf,
pub dry_run: bool,
pub shadow_enabled: bool,
pub message: Option<String>,
}
impl RefactoringExecutor {
pub fn apply(&self, plan: &RefactoringPlan) -> Result<Vec<String>, String> {
if plan.edits.is_empty() {
return Ok(vec![]);
}
let abs_paths: Vec<PathBuf> = plan.edits.iter().map(|e| e.file.clone()).collect();
if !self.dry_run && self.shadow_enabled {
let shadow = Shadow::new(&self.root);
if let Err(e) =
shadow.before_edit(&abs_paths.iter().map(|p| p.as_path()).collect::<Vec<_>>())
{
eprintln!("warning: shadow git: {}", e);
}
}
let mut modified: Vec<String> = vec![];
for edit in &plan.edits {
let rel_path = edit
.file
.strip_prefix(&self.root)
.unwrap_or(&edit.file)
.to_string_lossy()
.to_string();
if self.dry_run {
if !modified.contains(&rel_path) {
modified.push(rel_path);
}
} else {
match std::fs::write(&edit.file, &edit.new_content) {
Ok(_) => {
if !modified.contains(&rel_path) {
modified.push(rel_path);
}
}
Err(e) => eprintln!("error writing {}: {}", rel_path, e),
}
}
}
if !self.dry_run && self.shadow_enabled && !modified.is_empty() {
let shadow = Shadow::new(&self.root);
let info = EditInfo {
operation: plan.operation.clone(),
target: plan
.edits
.first()
.map(|e| e.description.clone())
.unwrap_or_default(),
files: abs_paths,
message: self.message.clone(),
workflow: None,
};
if let Err(e) = shadow.after_edit(&info) {
eprintln!("warning: shadow git: {}", e);
}
}
Ok(modified)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn executor_dry_run_does_not_write() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.rs");
std::fs::write(&file, "original").unwrap();
let executor = RefactoringExecutor {
root: dir.path().to_path_buf(),
dry_run: true,
shadow_enabled: false,
message: None,
};
let plan = RefactoringPlan {
operation: "test".to_string(),
edits: vec![PlannedEdit {
file: file.clone(),
original: "original".to_string(),
new_content: "modified".to_string(),
description: "test edit".to_string(),
}],
warnings: vec![],
};
let result = executor.apply(&plan).unwrap();
assert_eq!(result, vec!["test.rs"]);
assert_eq!(std::fs::read_to_string(&file).unwrap(), "original");
}
#[test]
fn executor_real_run_writes_files() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.rs");
std::fs::write(&file, "original").unwrap();
let executor = RefactoringExecutor {
root: dir.path().to_path_buf(),
dry_run: false,
shadow_enabled: false,
message: None,
};
let plan = RefactoringPlan {
operation: "test".to_string(),
edits: vec![PlannedEdit {
file: file.clone(),
original: "original".to_string(),
new_content: "modified".to_string(),
description: "test edit".to_string(),
}],
warnings: vec![],
};
let result = executor.apply(&plan).unwrap();
assert_eq!(result, vec!["test.rs"]);
assert_eq!(std::fs::read_to_string(&file).unwrap(), "modified");
}
#[test]
fn executor_deduplicates_modified_files() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.rs");
std::fs::write(&file, "original").unwrap();
let executor = RefactoringExecutor {
root: dir.path().to_path_buf(),
dry_run: false,
shadow_enabled: false,
message: None,
};
let plan = RefactoringPlan {
operation: "test".to_string(),
edits: vec![
PlannedEdit {
file: file.clone(),
original: "original".to_string(),
new_content: "step1".to_string(),
description: "edit 1".to_string(),
},
PlannedEdit {
file: file.clone(),
original: "step1".to_string(),
new_content: "step2".to_string(),
description: "edit 2".to_string(),
},
],
warnings: vec![],
};
let result = executor.apply(&plan).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(std::fs::read_to_string(&file).unwrap(), "step2");
}
#[test]
fn empty_plan_returns_empty() {
let dir = tempfile::tempdir().unwrap();
let executor = RefactoringExecutor {
root: dir.path().to_path_buf(),
dry_run: false,
shadow_enabled: false,
message: None,
};
let plan = RefactoringPlan {
operation: "test".to_string(),
edits: vec![],
warnings: vec![],
};
let result = executor.apply(&plan).unwrap();
assert!(result.is_empty());
}
}