1use crate::config::Config;
18use crate::core::{CollectorResult, ReplaceInfo};
19use crate::domain_types::{ModuleName, QualifiedName, Version};
20use crate::error::{DissolveError, Result};
21use crate::performance::PerformanceMonitor;
22use crate::type_introspection_context::TypeIntrospectionContext;
23use crate::types::TypeIntrospectionMethod;
24use std::collections::HashMap;
25use std::path::{Path, PathBuf};
26
27pub struct MigrationBuilder {
29 config: Config,
30 replacements: HashMap<QualifiedName, ReplaceInfo>,
31 source_paths: Vec<PathBuf>,
32 target_module: Option<ModuleName>,
33 performance_monitor: Option<PerformanceMonitor>,
34}
35
36impl MigrationBuilder {
37 pub fn new() -> Self {
39 Self {
40 config: Config::default(),
41 replacements: HashMap::new(),
42 source_paths: Vec::new(),
43 target_module: None,
44 performance_monitor: None,
45 }
46 }
47
48 pub fn with_config(mut self, config: Config) -> Self {
50 self.config = config;
51 self
52 }
53
54 pub fn with_type_introspection(mut self, method: TypeIntrospectionMethod) -> Self {
56 self.config.type_introspection = method;
57 self
58 }
59
60 pub fn with_sources<P: AsRef<Path>>(mut self, paths: impl IntoIterator<Item = P>) -> Self {
62 self.source_paths
63 .extend(paths.into_iter().map(|p| p.as_ref().to_path_buf()));
64 self
65 }
66
67 pub fn with_source<P: AsRef<Path>>(mut self, path: P) -> Self {
69 self.source_paths.push(path.as_ref().to_path_buf());
70 self
71 }
72
73 pub fn with_target_module(mut self, module: ModuleName) -> Self {
75 self.target_module = Some(module);
76 self
77 }
78
79 pub fn with_replacement(mut self, qualified_name: QualifiedName, info: ReplaceInfo) -> Self {
81 self.replacements.insert(qualified_name, info);
82 self
83 }
84
85 pub fn with_replacements_from_collector(mut self, collector_result: CollectorResult) -> Self {
87 for (name, info) in collector_result.replacements {
88 if let Ok(qualified_name) = QualifiedName::from_string(&name) {
89 self.replacements.insert(qualified_name, info);
90 }
91 }
92 self
93 }
94
95 pub fn with_performance_monitoring(mut self, enable: bool) -> Self {
97 if enable {
98 self.performance_monitor = Some(PerformanceMonitor::new(
99 self.config.performance.batch_size,
100 1000, ));
102 } else {
103 self.performance_monitor = None;
104 }
105 self
106 }
107
108 pub fn write_changes(mut self, write: bool) -> Self {
110 self.config.write_changes = write;
111 self
112 }
113
114 pub fn with_current_version(mut self, version: Version) -> Self {
116 self.config.current_version = Some(version);
117 self
118 }
119
120 pub fn execute(self) -> Result<MigrationResult> {
122 self.validate()?;
123
124 let mut type_context = TypeIntrospectionContext::new(self.config.type_introspection)
125 .map_err(|e| {
126 DissolveError::internal(format!("Failed to create type context: {}", e))
127 })?;
128 let mut results = MigrationResult::new();
129
130 for source_path in &self.source_paths {
132 if source_path.is_file() {
133 let result = self.process_file(source_path, &mut type_context)?;
134 results.merge(result);
135 } else if source_path.is_dir() {
136 let result = self.process_directory(source_path, &mut type_context)?;
137 results.merge(result);
138 } else {
139 return Err(DissolveError::invalid_input(format!(
140 "Path does not exist: {}",
141 source_path.display()
142 )));
143 }
144 }
145
146 type_context.shutdown().map_err(|e| {
147 DissolveError::internal(format!("Failed to shutdown type context: {}", e))
148 })?;
149
150 if let Some(monitor) = &self.performance_monitor {
152 results.performance_summary = Some(monitor.summary());
153 }
154
155 Ok(results)
156 }
157
158 fn validate(&self) -> Result<()> {
160 if self.source_paths.is_empty() {
161 return Err(DissolveError::invalid_input("No source paths specified"));
162 }
163
164 if self.replacements.is_empty() {
165 return Err(DissolveError::invalid_input("No replacements specified"));
166 }
167
168 self.config.validate()?;
169 Ok(())
170 }
171
172 fn process_file(
174 &self,
175 file_path: &Path,
176 type_context: &mut TypeIntrospectionContext,
177 ) -> Result<MigrationResult> {
178 let source = std::fs::read_to_string(file_path)?;
179 let module_name = self.detect_module_name(file_path)?;
180
181 let start = std::time::Instant::now();
182
183 let replacements_map: HashMap<String, ReplaceInfo> = self
185 .replacements
186 .iter()
187 .map(|(k, v)| (k.to_string(), v.clone()))
188 .collect();
189
190 let migrated_source = crate::migrate_stub::migrate_file(
191 &source,
192 &module_name.to_string(),
193 file_path,
194 type_context,
195 replacements_map,
196 HashMap::new(), )
198 .map_err(|e| DissolveError::internal(format!("Migration failed: {}", e)))?;
199
200 let elapsed = start.elapsed();
201
202 if let Some(monitor) = &self.performance_monitor {
203 monitor.record_file_processed();
204 monitor.record_migration_time(elapsed);
205 }
206
207 let mut result = MigrationResult::new();
208 result.files_processed.push(FileResult {
209 path: file_path.to_path_buf(),
210 original_size: source.len(),
211 migrated_size: migrated_source.len(),
212 processing_time: elapsed,
213 changes_made: source != migrated_source,
214 });
215
216 if self.config.write_changes && source != migrated_source {
217 if self.config.create_backups {
218 let backup_path = format!("{}.bak", file_path.display());
219 std::fs::copy(file_path, &backup_path)?;
220 result.backups_created.push(PathBuf::from(backup_path));
221 }
222
223 std::fs::write(file_path, &migrated_source)?;
224 result.files_written += 1;
225 }
226
227 Ok(result)
228 }
229
230 fn process_directory(
232 &self,
233 dir_path: &Path,
234 type_context: &mut TypeIntrospectionContext,
235 ) -> Result<MigrationResult> {
236 let mut results = MigrationResult::new();
237
238 for entry in walkdir::WalkDir::new(dir_path) {
239 let entry = entry.map_err(|e| DissolveError::Io(e.into()))?;
240 let path = entry.path();
241
242 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("py") {
243 if let Ok(module_name) = self.detect_module_name(path) {
245 if self.config.excluded_modules.contains(&module_name) {
246 continue;
247 }
248 }
249
250 let file_result = self.process_file(path, type_context)?;
251 results.merge(file_result);
252 }
253 }
254
255 Ok(results)
256 }
257
258 fn detect_module_name(&self, file_path: &Path) -> Result<ModuleName> {
260 let stem = file_path
262 .file_stem()
263 .and_then(|s| s.to_str())
264 .ok_or_else(|| DissolveError::invalid_input("Invalid file path"))?;
265
266 if stem == "__init__" {
267 let parent = file_path
269 .parent()
270 .and_then(|p| p.file_name())
271 .and_then(|n| n.to_str())
272 .ok_or_else(|| DissolveError::invalid_input("Cannot determine module name"))?;
273 Ok(ModuleName::new(parent))
274 } else {
275 Ok(ModuleName::new(stem))
276 }
277 }
278}
279
280impl Default for MigrationBuilder {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286pub struct CollectionBuilder {
288 config: Config,
289 source_paths: Vec<PathBuf>,
290 target_modules: Vec<ModuleName>,
291}
292
293impl CollectionBuilder {
294 pub fn new() -> Self {
295 Self {
296 config: Config::default(),
297 source_paths: Vec::new(),
298 target_modules: Vec::new(),
299 }
300 }
301
302 pub fn with_config(mut self, config: Config) -> Self {
303 self.config = config;
304 self
305 }
306
307 pub fn with_sources<P: AsRef<Path>>(mut self, paths: impl IntoIterator<Item = P>) -> Self {
308 self.source_paths
309 .extend(paths.into_iter().map(|p| p.as_ref().to_path_buf()));
310 self
311 }
312
313 pub fn with_target_modules(mut self, modules: impl IntoIterator<Item = ModuleName>) -> Self {
314 self.target_modules.extend(modules);
315 self
316 }
317
318 pub fn execute(self) -> Result<CollectorResult> {
319 self.validate()?;
320
321 let mut all_replacements = HashMap::new();
322 let mut all_unreplaceable = HashMap::new();
323 let mut all_imports = Vec::new();
324
325 for source_path in &self.source_paths {
326 if source_path.is_file() {
327 let result = self.collect_from_file(source_path)?;
328 all_replacements.extend(result.replacements);
329 all_unreplaceable.extend(result.unreplaceable);
330 all_imports.extend(result.imports);
331 } else if source_path.is_dir() {
332 let result = self.collect_from_directory(source_path)?;
333 all_replacements.extend(result.replacements);
334 all_unreplaceable.extend(result.unreplaceable);
335 all_imports.extend(result.imports);
336 }
337 }
338
339 Ok(CollectorResult {
340 replacements: all_replacements,
341 unreplaceable: all_unreplaceable,
342 imports: all_imports,
343 class_methods: HashMap::new(),
344 inheritance_map: HashMap::new(),
345 })
346 }
347
348 fn validate(&self) -> Result<()> {
349 if self.source_paths.is_empty() {
350 return Err(DissolveError::invalid_input("No source paths specified"));
351 }
352 Ok(())
353 }
354
355 fn collect_from_file(&self, file_path: &Path) -> Result<CollectorResult> {
356 let source = std::fs::read_to_string(file_path)?;
357 let module_name = self.detect_module_name(file_path)?;
358
359 let collector = crate::stub_collector::RuffDeprecatedFunctionCollector::new(
361 module_name.to_string(),
362 None,
363 );
364
365 collector
366 .collect_from_source(source)
367 .map_err(|e| DissolveError::internal(format!("Collection failed: {}", e)))
368 }
369
370 fn collect_from_directory(&self, dir_path: &Path) -> Result<CollectorResult> {
371 let mut all_replacements = HashMap::new();
372 let mut all_unreplaceable = HashMap::new();
373 let mut all_imports = Vec::new();
374
375 for entry in walkdir::WalkDir::new(dir_path) {
376 let entry = entry.map_err(|e| DissolveError::Io(e.into()))?;
377 let path = entry.path();
378
379 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("py") {
380 let result = self.collect_from_file(path)?;
381 all_replacements.extend(result.replacements);
382 all_unreplaceable.extend(result.unreplaceable);
383 all_imports.extend(result.imports);
384 }
385 }
386
387 Ok(CollectorResult {
388 replacements: all_replacements,
389 unreplaceable: all_unreplaceable,
390 imports: all_imports,
391 class_methods: HashMap::new(),
392 inheritance_map: HashMap::new(),
393 })
394 }
395
396 fn detect_module_name(&self, file_path: &Path) -> Result<ModuleName> {
397 let stem = file_path
398 .file_stem()
399 .and_then(|s| s.to_str())
400 .ok_or_else(|| DissolveError::invalid_input("Invalid file path"))?;
401 Ok(ModuleName::new(stem))
402 }
403}
404
405impl Default for CollectionBuilder {
406 fn default() -> Self {
407 Self::new()
408 }
409}
410
411#[derive(Debug)]
413pub struct MigrationResult {
414 pub files_processed: Vec<FileResult>,
415 pub files_written: usize,
416 pub backups_created: Vec<PathBuf>,
417 pub performance_summary: Option<crate::performance::PerformanceSummary>,
418}
419
420impl MigrationResult {
421 pub fn new() -> Self {
422 Self {
423 files_processed: Vec::new(),
424 files_written: 0,
425 backups_created: Vec::new(),
426 performance_summary: None,
427 }
428 }
429
430 pub fn merge(&mut self, other: MigrationResult) {
431 self.files_processed.extend(other.files_processed);
432 self.files_written += other.files_written;
433 self.backups_created.extend(other.backups_created);
434 }
436
437 pub fn total_files(&self) -> usize {
438 self.files_processed.len()
439 }
440
441 pub fn files_with_changes(&self) -> usize {
442 self.files_processed
443 .iter()
444 .filter(|f| f.changes_made)
445 .count()
446 }
447
448 pub fn total_processing_time(&self) -> std::time::Duration {
449 self.files_processed.iter().map(|f| f.processing_time).sum()
450 }
451}
452
453impl Default for MigrationResult {
454 fn default() -> Self {
455 Self::new()
456 }
457}
458
459#[derive(Debug)]
460pub struct FileResult {
461 pub path: PathBuf,
462 pub original_size: usize,
463 pub migrated_size: usize,
464 pub processing_time: std::time::Duration,
465 pub changes_made: bool,
466}
467
468impl std::fmt::Display for MigrationResult {
469 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470 writeln!(f, "Migration Results:")?;
471 writeln!(f, " Files processed: {}", self.total_files())?;
472 writeln!(f, " Files with changes: {}", self.files_with_changes())?;
473 writeln!(f, " Files written: {}", self.files_written)?;
474 writeln!(f, " Backups created: {}", self.backups_created.len())?;
475 writeln!(
476 f,
477 " Total processing time: {:.2?}",
478 self.total_processing_time()
479 )?;
480
481 if let Some(perf) = &self.performance_summary {
482 write!(f, "\n{}", perf)?;
483 }
484
485 Ok(())
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use tempfile::TempDir;
493
494 #[test]
495 fn test_migration_builder() {
496 let builder = MigrationBuilder::new()
497 .with_type_introspection(TypeIntrospectionMethod::MypyDaemon)
498 .write_changes(false)
499 .with_performance_monitoring(true);
500
501 assert!(builder.validate().is_err());
503 }
504
505 #[test]
506 fn test_collection_builder() {
507 let temp_dir = TempDir::new().unwrap();
508 let test_file = temp_dir.path().join("test.py");
509 std::fs::write(&test_file, "# empty file").unwrap();
510
511 let builder = CollectionBuilder::new().with_sources(vec![test_file]);
512
513 assert!(builder.validate().is_ok());
514 }
515
516 #[test]
517 fn test_migration_result_display() {
518 let mut result = MigrationResult::new();
519 result.files_processed.push(FileResult {
520 path: PathBuf::from("test.py"),
521 original_size: 100,
522 migrated_size: 95,
523 processing_time: std::time::Duration::from_millis(10),
524 changes_made: true,
525 });
526 result.files_written = 1;
527
528 let display = format!("{}", result);
529 assert!(display.contains("Files processed: 1"));
530 assert!(display.contains("Files with changes: 1"));
531 }
532}