use crate::config::Config;
use crate::core::{CollectorResult, ReplaceInfo};
use crate::domain_types::{ModuleName, QualifiedName, Version};
use crate::error::{DissolveError, Result};
use crate::performance::PerformanceMonitor;
use crate::type_introspection_context::TypeIntrospectionContext;
use crate::types::TypeIntrospectionMethod;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub struct MigrationBuilder {
config: Config,
replacements: HashMap<QualifiedName, ReplaceInfo>,
source_paths: Vec<PathBuf>,
target_module: Option<ModuleName>,
performance_monitor: Option<PerformanceMonitor>,
}
impl MigrationBuilder {
pub fn new() -> Self {
Self {
config: Config::default(),
replacements: HashMap::new(),
source_paths: Vec::new(),
target_module: None,
performance_monitor: None,
}
}
pub fn with_config(mut self, config: Config) -> Self {
self.config = config;
self
}
pub fn with_type_introspection(mut self, method: TypeIntrospectionMethod) -> Self {
self.config.type_introspection = method;
self
}
pub fn with_sources<P: AsRef<Path>>(mut self, paths: impl IntoIterator<Item = P>) -> Self {
self.source_paths
.extend(paths.into_iter().map(|p| p.as_ref().to_path_buf()));
self
}
pub fn with_source<P: AsRef<Path>>(mut self, path: P) -> Self {
self.source_paths.push(path.as_ref().to_path_buf());
self
}
pub fn with_target_module(mut self, module: ModuleName) -> Self {
self.target_module = Some(module);
self
}
pub fn with_replacement(mut self, qualified_name: QualifiedName, info: ReplaceInfo) -> Self {
self.replacements.insert(qualified_name, info);
self
}
pub fn with_replacements_from_collector(mut self, collector_result: CollectorResult) -> Self {
for (name, info) in collector_result.replacements {
if let Ok(qualified_name) = QualifiedName::from_string(&name) {
self.replacements.insert(qualified_name, info);
}
}
self
}
pub fn with_performance_monitoring(mut self, enable: bool) -> Self {
if enable {
self.performance_monitor = Some(PerformanceMonitor::new(
self.config.performance.batch_size,
1000, ));
} else {
self.performance_monitor = None;
}
self
}
pub fn write_changes(mut self, write: bool) -> Self {
self.config.write_changes = write;
self
}
pub fn with_current_version(mut self, version: Version) -> Self {
self.config.current_version = Some(version);
self
}
pub fn execute(self) -> Result<MigrationResult> {
self.validate()?;
let mut type_context = TypeIntrospectionContext::new(self.config.type_introspection)
.map_err(|e| {
DissolveError::internal(format!("Failed to create type context: {}", e))
})?;
let mut results = MigrationResult::new();
for source_path in &self.source_paths {
if source_path.is_file() {
let result = self.process_file(source_path, &mut type_context)?;
results.merge(result);
} else if source_path.is_dir() {
let result = self.process_directory(source_path, &mut type_context)?;
results.merge(result);
} else {
return Err(DissolveError::invalid_input(format!(
"Path does not exist: {}",
source_path.display()
)));
}
}
type_context.shutdown().map_err(|e| {
DissolveError::internal(format!("Failed to shutdown type context: {}", e))
})?;
if let Some(monitor) = &self.performance_monitor {
results.performance_summary = Some(monitor.summary());
}
Ok(results)
}
fn validate(&self) -> Result<()> {
if self.source_paths.is_empty() {
return Err(DissolveError::invalid_input("No source paths specified"));
}
if self.replacements.is_empty() {
return Err(DissolveError::invalid_input("No replacements specified"));
}
self.config.validate()?;
Ok(())
}
fn process_file(
&self,
file_path: &Path,
type_context: &mut TypeIntrospectionContext,
) -> Result<MigrationResult> {
let source = std::fs::read_to_string(file_path)?;
let module_name = self.detect_module_name(file_path)?;
let start = std::time::Instant::now();
let replacements_map: HashMap<String, ReplaceInfo> = self
.replacements
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
let migrated_source = crate::migrate_stub::migrate_file(
&source,
&module_name.to_string(),
file_path,
type_context,
replacements_map,
HashMap::new(), )
.map_err(|e| DissolveError::internal(format!("Migration failed: {}", e)))?;
let elapsed = start.elapsed();
if let Some(monitor) = &self.performance_monitor {
monitor.record_file_processed();
monitor.record_migration_time(elapsed);
}
let mut result = MigrationResult::new();
result.files_processed.push(FileResult {
path: file_path.to_path_buf(),
original_size: source.len(),
migrated_size: migrated_source.len(),
processing_time: elapsed,
changes_made: source != migrated_source,
});
if self.config.write_changes && source != migrated_source {
if self.config.create_backups {
let backup_path = format!("{}.bak", file_path.display());
std::fs::copy(file_path, &backup_path)?;
result.backups_created.push(PathBuf::from(backup_path));
}
std::fs::write(file_path, &migrated_source)?;
result.files_written += 1;
}
Ok(result)
}
fn process_directory(
&self,
dir_path: &Path,
type_context: &mut TypeIntrospectionContext,
) -> Result<MigrationResult> {
let mut results = MigrationResult::new();
for entry in walkdir::WalkDir::new(dir_path) {
let entry = entry.map_err(|e| DissolveError::Io(e.into()))?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("py") {
if let Ok(module_name) = self.detect_module_name(path) {
if self.config.excluded_modules.contains(&module_name) {
continue;
}
}
let file_result = self.process_file(path, type_context)?;
results.merge(file_result);
}
}
Ok(results)
}
fn detect_module_name(&self, file_path: &Path) -> Result<ModuleName> {
let stem = file_path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| DissolveError::invalid_input("Invalid file path"))?;
if stem == "__init__" {
let parent = file_path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.ok_or_else(|| DissolveError::invalid_input("Cannot determine module name"))?;
Ok(ModuleName::new(parent))
} else {
Ok(ModuleName::new(stem))
}
}
}
impl Default for MigrationBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct CollectionBuilder {
config: Config,
source_paths: Vec<PathBuf>,
target_modules: Vec<ModuleName>,
}
impl CollectionBuilder {
pub fn new() -> Self {
Self {
config: Config::default(),
source_paths: Vec::new(),
target_modules: Vec::new(),
}
}
pub fn with_config(mut self, config: Config) -> Self {
self.config = config;
self
}
pub fn with_sources<P: AsRef<Path>>(mut self, paths: impl IntoIterator<Item = P>) -> Self {
self.source_paths
.extend(paths.into_iter().map(|p| p.as_ref().to_path_buf()));
self
}
pub fn with_target_modules(mut self, modules: impl IntoIterator<Item = ModuleName>) -> Self {
self.target_modules.extend(modules);
self
}
pub fn execute(self) -> Result<CollectorResult> {
self.validate()?;
let mut all_replacements = HashMap::new();
let mut all_unreplaceable = HashMap::new();
let mut all_imports = Vec::new();
for source_path in &self.source_paths {
if source_path.is_file() {
let result = self.collect_from_file(source_path)?;
all_replacements.extend(result.replacements);
all_unreplaceable.extend(result.unreplaceable);
all_imports.extend(result.imports);
} else if source_path.is_dir() {
let result = self.collect_from_directory(source_path)?;
all_replacements.extend(result.replacements);
all_unreplaceable.extend(result.unreplaceable);
all_imports.extend(result.imports);
}
}
Ok(CollectorResult {
replacements: all_replacements,
unreplaceable: all_unreplaceable,
imports: all_imports,
class_methods: HashMap::new(),
inheritance_map: HashMap::new(),
})
}
fn validate(&self) -> Result<()> {
if self.source_paths.is_empty() {
return Err(DissolveError::invalid_input("No source paths specified"));
}
Ok(())
}
fn collect_from_file(&self, file_path: &Path) -> Result<CollectorResult> {
let source = std::fs::read_to_string(file_path)?;
let module_name = self.detect_module_name(file_path)?;
let collector = crate::stub_collector::RuffDeprecatedFunctionCollector::new(
module_name.to_string(),
None,
);
collector
.collect_from_source(source)
.map_err(|e| DissolveError::internal(format!("Collection failed: {}", e)))
}
fn collect_from_directory(&self, dir_path: &Path) -> Result<CollectorResult> {
let mut all_replacements = HashMap::new();
let mut all_unreplaceable = HashMap::new();
let mut all_imports = Vec::new();
for entry in walkdir::WalkDir::new(dir_path) {
let entry = entry.map_err(|e| DissolveError::Io(e.into()))?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("py") {
let result = self.collect_from_file(path)?;
all_replacements.extend(result.replacements);
all_unreplaceable.extend(result.unreplaceable);
all_imports.extend(result.imports);
}
}
Ok(CollectorResult {
replacements: all_replacements,
unreplaceable: all_unreplaceable,
imports: all_imports,
class_methods: HashMap::new(),
inheritance_map: HashMap::new(),
})
}
fn detect_module_name(&self, file_path: &Path) -> Result<ModuleName> {
let stem = file_path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| DissolveError::invalid_input("Invalid file path"))?;
Ok(ModuleName::new(stem))
}
}
impl Default for CollectionBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct MigrationResult {
pub files_processed: Vec<FileResult>,
pub files_written: usize,
pub backups_created: Vec<PathBuf>,
pub performance_summary: Option<crate::performance::PerformanceSummary>,
}
impl MigrationResult {
pub fn new() -> Self {
Self {
files_processed: Vec::new(),
files_written: 0,
backups_created: Vec::new(),
performance_summary: None,
}
}
pub fn merge(&mut self, other: MigrationResult) {
self.files_processed.extend(other.files_processed);
self.files_written += other.files_written;
self.backups_created.extend(other.backups_created);
}
pub fn total_files(&self) -> usize {
self.files_processed.len()
}
pub fn files_with_changes(&self) -> usize {
self.files_processed
.iter()
.filter(|f| f.changes_made)
.count()
}
pub fn total_processing_time(&self) -> std::time::Duration {
self.files_processed.iter().map(|f| f.processing_time).sum()
}
}
impl Default for MigrationResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct FileResult {
pub path: PathBuf,
pub original_size: usize,
pub migrated_size: usize,
pub processing_time: std::time::Duration,
pub changes_made: bool,
}
impl std::fmt::Display for MigrationResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Migration Results:")?;
writeln!(f, " Files processed: {}", self.total_files())?;
writeln!(f, " Files with changes: {}", self.files_with_changes())?;
writeln!(f, " Files written: {}", self.files_written)?;
writeln!(f, " Backups created: {}", self.backups_created.len())?;
writeln!(
f,
" Total processing time: {:.2?}",
self.total_processing_time()
)?;
if let Some(perf) = &self.performance_summary {
write!(f, "\n{}", perf)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_migration_builder() {
let builder = MigrationBuilder::new()
.with_type_introspection(TypeIntrospectionMethod::MypyDaemon)
.write_changes(false)
.with_performance_monitoring(true);
assert!(builder.validate().is_err());
}
#[test]
fn test_collection_builder() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.py");
std::fs::write(&test_file, "# empty file").unwrap();
let builder = CollectionBuilder::new().with_sources(vec![test_file]);
assert!(builder.validate().is_ok());
}
#[test]
fn test_migration_result_display() {
let mut result = MigrationResult::new();
result.files_processed.push(FileResult {
path: PathBuf::from("test.py"),
original_size: 100,
migrated_size: 95,
processing_time: std::time::Duration::from_millis(10),
changes_made: true,
});
result.files_written = 1;
let display = format!("{}", result);
assert!(display.contains("Files processed: 1"));
assert!(display.contains("Files with changes: 1"));
}
}