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