Skip to main content

dissolve_python/
builder.rs

1// Copyright (C) 2024 Jelmer Vernooij <jelmer@samba.org>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Builder patterns for creating complex dissolve operations
16
17use 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
27/// Builder for migration operations
28pub 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    /// Create a new migration builder
38    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    /// Set the configuration
49    pub fn with_config(mut self, config: Config) -> Self {
50        self.config = config;
51        self
52    }
53
54    /// Set the type introspection method
55    pub fn with_type_introspection(mut self, method: TypeIntrospectionMethod) -> Self {
56        self.config.type_introspection = method;
57        self
58    }
59
60    /// Add source paths to process
61    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    /// Add a single source path
68    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    /// Set the target module for replacements
74    pub fn with_target_module(mut self, module: ModuleName) -> Self {
75        self.target_module = Some(module);
76        self
77    }
78
79    /// Add replacement info
80    pub fn with_replacement(mut self, qualified_name: QualifiedName, info: ReplaceInfo) -> Self {
81        self.replacements.insert(qualified_name, info);
82        self
83    }
84
85    /// Add multiple replacements from a collector result
86    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    /// Enable performance monitoring
96    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, // Default type cache size
101            ));
102        } else {
103            self.performance_monitor = None;
104        }
105        self
106    }
107
108    /// Enable writing changes to files
109    pub fn write_changes(mut self, write: bool) -> Self {
110        self.config.write_changes = write;
111        self
112    }
113
114    /// Set current version for version-based operations
115    pub fn with_current_version(mut self, version: Version) -> Self {
116        self.config.current_version = Some(version);
117        self
118    }
119
120    /// Build and execute the migration
121    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        // Process each source path
131        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        // Add performance summary if monitoring was enabled
151        if let Some(monitor) = &self.performance_monitor {
152            results.performance_summary = Some(monitor.summary());
153        }
154
155        Ok(results)
156    }
157
158    /// Validate the builder configuration
159    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    /// Process a single file
173    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        // Use the migration logic from existing modules
184        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(), // Empty unreplaceable map
197        )
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    /// Process a directory recursively
231    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                // Check if this module should be excluded
244                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    /// Detect module name from file path
259    fn detect_module_name(&self, file_path: &Path) -> Result<ModuleName> {
260        // Simple implementation - in practice this would be more sophisticated
261        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            // Use parent directory name
268            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
286/// Builder for collection operations (finding deprecated functions)
287pub 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        // Use existing collector
360        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/// Result of a migration operation
412#[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        // Performance summary would be combined differently in a real implementation
435    }
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        // Validation should fail because no sources or replacements
502        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}