use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use crate::core::{ReplaceInfo, RuffDeprecatedFunctionCollector};
use crate::rustpython_visitor::RustPythonFunctionCallReplacer;
use crate::type_introspection_context::TypeIntrospectionContext;
pub fn migrate_file(
source: &str,
module_name: &str,
file_path: &Path,
type_introspection_context: &mut TypeIntrospectionContext,
mut replacements: HashMap<String, ReplaceInfo>,
dependency_inheritance_map: HashMap<String, Vec<String>>,
) -> Result<String> {
tracing::info!("migrate_ruff::migrate_file called for module {}", module_name);
use crate::unified_visitor::{UnifiedVisitor, UnifiedResult};
let visitor = UnifiedVisitor::new_for_collection(module_name, Some(file_path));
let builtins = visitor.get_builtins().clone();
let unified_result = visitor.process_source(source.to_string())?;
let collector_result = match unified_result {
UnifiedResult::Collection(collection_result) => collection_result,
_ => return Err(anyhow::anyhow!("Expected collection result from UnifiedVisitor")),
};
for (key, value) in collector_result.replacements {
replacements.insert(key, value);
}
tracing::info!("Found {} replacements: {:?}", replacements.len(), replacements.keys().collect::<Vec<_>>());
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.map_err(|e| anyhow::anyhow!("Failed to parse: {:?}", e))?;
let mut merged_inheritance_map = collector_result.inheritance_map;
merged_inheritance_map.extend(dependency_inheritance_map);
let mut replacer = RustPythonFunctionCallReplacer::new_with_context(
replacements,
type_introspection_context,
file_path.to_string_lossy().into_owned(),
module_name.to_string(),
source.to_string(),
merged_inheritance_map,
builtins,
)?;
if let rustpython_ast::Mod::Module(module) = ast {
tracing::debug!("Visiting {} statements in module", module.body.len());
replacer.visit_module(&module.body);
}
let replacements = replacer.get_replacements();
tracing::debug!("Visitor found {} replacements", replacements.len());
tracing::debug!("Applying {} replacements", replacements.len());
for (range, replacement) in &replacements {
let original = &source[range.start().to_usize()..range.end().to_usize()];
tracing::debug!("Replacing '{}' with '{}'", original, replacement);
}
let migrated_source = crate::ruff_parser::apply_replacements(source, replacements.clone());
if let Err(e) = rustpython_parser::parse(&migrated_source, rustpython_parser::Mode::Module, "<test>") {
tracing::error!("Generated invalid Python: {}", e);
tracing::error!("Migrated source:\n{}", migrated_source);
}
if !replacements.is_empty() {
type_introspection_context.update_file(file_path, &migrated_source)?;
}
Ok(migrated_source)
}
pub fn migrate_file_interactive(
source: &str,
module_name: &str,
file_path: &Path,
type_introspection_context: &mut TypeIntrospectionContext,
replacements: HashMap<String, ReplaceInfo>,
dependency_inheritance_map: HashMap<String, Vec<String>>,
) -> Result<String> {
migrate_file(
source,
module_name,
file_path,
type_introspection_context,
replacements,
dependency_inheritance_map,
)
}
pub fn check_file(
source: &str,
module_name: &str,
file_path: &Path,
) -> Result<crate::checker::CheckResult> {
use crate::unified_visitor::{UnifiedVisitor, UnifiedResult};
let visitor = UnifiedVisitor::new_for_collection(module_name, Some(file_path));
let unified_result = visitor.process_source(source.to_string())?;
let result = match unified_result {
UnifiedResult::Collection(collection_result) => collection_result,
_ => return Err(anyhow::anyhow!("Expected collection result from UnifiedVisitor")),
};
let mut check_result = crate::checker::CheckResult::new();
for func_name in result.replacements.keys() {
check_result.checked_functions.push(func_name.clone());
}
for func_name in result.unreplaceable.keys() {
check_result.checked_functions.push(func_name.clone());
}
for (func_name, unreplaceable) in result.unreplaceable {
check_result.add_error(format!(
"Function '{}' cannot be replaced: {:?}",
func_name, unreplaceable.reason
));
}
Ok(check_result)
}
pub fn remove_decorators(
source: &str,
_before_version: Option<&str>,
_module_name: &str,
) -> Result<String> {
Ok(source.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
pub fn migrate_file_with_method(
source: &str,
module_name: &str,
file_path: String,
method: crate::types::TypeIntrospectionMethod,
replacements: HashMap<String, ReplaceInfo>,
) -> Result<String> {
let mut context = TypeIntrospectionContext::new(method)?;
migrate_file(
source,
module_name,
Path::new(&file_path),
&mut context,
replacements,
HashMap::new(),
)
}
#[test]
fn test_migrate_simple_function() {
let source = r#"
from dissolve import replace_me
@replace_me()
def old_func(x, y):
return new_func(x * 2, y + 1)
result = old_func(5, 10)
"#;
let collector = RuffDeprecatedFunctionCollector::new("test_module".to_string(), None);
let result = collector.collect_from_source(source.to_string()).unwrap();
println!(
"Collected replacements: {:?}",
result.replacements.keys().collect::<Vec<_>>()
);
for (name, info) in &result.replacements {
println!(" {} -> {}", name, info.replacement_expr);
}
let test_ctx = crate::tests::test_utils::TestContext::new(source);
let mut type_context =
TypeIntrospectionContext::new(crate::types::TypeIntrospectionMethod::PyrightLsp)
.unwrap();
let migrated = migrate_file(
source,
"test_module",
Path::new(&test_ctx.file_path),
&mut type_context,
result.replacements,
HashMap::new(),
)
.unwrap();
println!("Original:\n{}", source);
println!("\nMigrated:\n{}", migrated);
assert!(migrated.contains("new_func(5 * 2, 10 + 1)"));
assert!(!migrated.contains("result = old_func(5, 10)"));
}
}