1pub mod actions;
9pub mod add_parameter;
10pub mod inline_function;
11pub mod inline_variable;
12pub mod introduce_variable;
13pub mod move_item;
14pub mod rename;
15
16use std::path::PathBuf;
17
18use normalize_shadow::{EditInfo, Shadow};
19
20pub struct PlannedEdit {
22 pub file: PathBuf,
23 pub original: String,
24 pub new_content: String,
25 pub description: String,
26}
27
28pub struct RefactoringPlan {
30 pub operation: String,
31 pub edits: Vec<PlannedEdit>,
32 pub warnings: Vec<String>,
33}
34
35pub struct RefactoringContext {
37 pub root: PathBuf,
38 pub editor: normalize_edit::Editor,
39 pub index: Option<normalize_facts::FileIndex>,
40 pub loader: normalize_languages::GrammarLoader,
41}
42
43pub struct References {
45 pub callers: Vec<CallerRef>,
46 pub importers: Vec<ImportRef>,
47}
48
49pub struct CallerRef {
51 pub file: String,
52 pub caller: String,
53 pub line: usize,
54 #[allow(dead_code)]
55 pub access: Option<String>,
56}
57
58pub struct ImportRef {
60 pub file: String,
61 pub name: String,
62 #[allow(dead_code)]
63 pub alias: Option<String>,
64 pub line: usize,
65}
66
67pub struct RefactoringExecutor {
69 pub root: PathBuf,
70 pub dry_run: bool,
71 pub shadow_enabled: bool,
72 pub message: Option<String>,
73}
74
75impl RefactoringExecutor {
76 pub fn apply(&self, plan: &RefactoringPlan) -> Result<Vec<String>, String> {
79 if plan.edits.is_empty() {
80 return Ok(vec![]);
81 }
82
83 let abs_paths: Vec<PathBuf> = plan.edits.iter().map(|e| e.file.clone()).collect();
84
85 if !self.dry_run && self.shadow_enabled {
87 let shadow = Shadow::new(&self.root);
88 if let Err(e) =
89 shadow.before_edit(&abs_paths.iter().map(|p| p.as_path()).collect::<Vec<_>>())
90 {
91 eprintln!("warning: shadow git: {}", e);
92 }
93 }
94
95 let mut modified: Vec<String> = vec![];
96
97 for edit in &plan.edits {
98 let rel_path = edit
99 .file
100 .strip_prefix(&self.root)
101 .unwrap_or(&edit.file)
102 .to_string_lossy()
103 .to_string();
104
105 if self.dry_run {
106 if !modified.contains(&rel_path) {
107 modified.push(rel_path);
108 }
109 } else {
110 match std::fs::write(&edit.file, &edit.new_content) {
111 Ok(_) => {
112 if !modified.contains(&rel_path) {
113 modified.push(rel_path);
114 }
115 }
116 Err(e) => eprintln!("error writing {}: {}", rel_path, e),
117 }
118 }
119 }
120
121 if !self.dry_run && self.shadow_enabled && !modified.is_empty() {
123 let shadow = Shadow::new(&self.root);
124 let info = EditInfo {
125 operation: plan.operation.clone(),
126 target: plan
127 .edits
128 .first()
129 .map(|e| e.description.clone())
130 .unwrap_or_default(),
131 files: abs_paths,
132 message: self.message.clone(),
133 workflow: None,
134 };
135 if let Err(e) = shadow.after_edit(&info) {
136 eprintln!("warning: shadow git: {}", e);
137 }
138 }
139
140 Ok(modified)
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn executor_dry_run_does_not_write() {
150 let dir = tempfile::tempdir().unwrap();
151 let file = dir.path().join("test.rs");
152 std::fs::write(&file, "original").unwrap();
153
154 let executor = RefactoringExecutor {
155 root: dir.path().to_path_buf(),
156 dry_run: true,
157 shadow_enabled: false,
158 message: None,
159 };
160
161 let plan = RefactoringPlan {
162 operation: "test".to_string(),
163 edits: vec![PlannedEdit {
164 file: file.clone(),
165 original: "original".to_string(),
166 new_content: "modified".to_string(),
167 description: "test edit".to_string(),
168 }],
169 warnings: vec![],
170 };
171
172 let result = executor.apply(&plan).unwrap();
173 assert_eq!(result, vec!["test.rs"]);
174 assert_eq!(std::fs::read_to_string(&file).unwrap(), "original");
176 }
177
178 #[test]
179 fn executor_real_run_writes_files() {
180 let dir = tempfile::tempdir().unwrap();
181 let file = dir.path().join("test.rs");
182 std::fs::write(&file, "original").unwrap();
183
184 let executor = RefactoringExecutor {
185 root: dir.path().to_path_buf(),
186 dry_run: false,
187 shadow_enabled: false,
188 message: None,
189 };
190
191 let plan = RefactoringPlan {
192 operation: "test".to_string(),
193 edits: vec![PlannedEdit {
194 file: file.clone(),
195 original: "original".to_string(),
196 new_content: "modified".to_string(),
197 description: "test edit".to_string(),
198 }],
199 warnings: vec![],
200 };
201
202 let result = executor.apply(&plan).unwrap();
203 assert_eq!(result, vec!["test.rs"]);
204 assert_eq!(std::fs::read_to_string(&file).unwrap(), "modified");
205 }
206
207 #[test]
208 fn executor_deduplicates_modified_files() {
209 let dir = tempfile::tempdir().unwrap();
210 let file = dir.path().join("test.rs");
211 std::fs::write(&file, "original").unwrap();
212
213 let executor = RefactoringExecutor {
214 root: dir.path().to_path_buf(),
215 dry_run: false,
216 shadow_enabled: false,
217 message: None,
218 };
219
220 let plan = RefactoringPlan {
221 operation: "test".to_string(),
222 edits: vec![
223 PlannedEdit {
224 file: file.clone(),
225 original: "original".to_string(),
226 new_content: "step1".to_string(),
227 description: "edit 1".to_string(),
228 },
229 PlannedEdit {
230 file: file.clone(),
231 original: "step1".to_string(),
232 new_content: "step2".to_string(),
233 description: "edit 2".to_string(),
234 },
235 ],
236 warnings: vec![],
237 };
238
239 let result = executor.apply(&plan).unwrap();
240 assert_eq!(result.len(), 1);
241 assert_eq!(std::fs::read_to_string(&file).unwrap(), "step2");
242 }
243
244 #[test]
245 fn empty_plan_returns_empty() {
246 let dir = tempfile::tempdir().unwrap();
247 let executor = RefactoringExecutor {
248 root: dir.path().to_path_buf(),
249 dry_run: false,
250 shadow_enabled: false,
251 message: None,
252 };
253
254 let plan = RefactoringPlan {
255 operation: "test".to_string(),
256 edits: vec![],
257 warnings: vec![],
258 };
259
260 let result = executor.apply(&plan).unwrap();
261 assert!(result.is_empty());
262 }
263}