dissolve-python 0.3.0

A tool to dissolve deprecated calls in Python codebases
Documentation
// Implementation of migrate_ruff functions using rustpython-parser
use crate::checker::CheckResult;
use crate::core::ReplaceInfo;
use crate::ruff_parser::apply_replacements;
use crate::rustpython_visitor::RustPythonFunctionCallReplacer;
use crate::stub_collector::RuffDeprecatedFunctionCollector;
use anyhow::Result;
use std::collections::HashMap;

pub fn migrate_file(
    source: &str,
    module_name: &str,
    file_path: &std::path::Path,
    type_context: &mut crate::type_introspection_context::TypeIntrospectionContext,
    mut all_replacements: HashMap<String, ReplaceInfo>,
    dependency_inheritance_map: HashMap<String, Vec<String>>,
) -> Result<String> {
    // Open the file in the type introspection context so we can query types
    type_context.open_file(file_path, source)?;

    // Always collect from source to get inheritance information
    let collector = RuffDeprecatedFunctionCollector::new(module_name.to_string(), Some(file_path));
    let builtins = collector.builtins().clone();
    let collector_result = collector.collect_from_source(source.to_string())?;

    // Merge provided replacements with ones collected from the source file
    // Source file replacements take priority over dependency replacements
    for (key, value) in collector_result.replacements {
        all_replacements.insert(key, value);
    }

    // Parse source with rustpython
    let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
        .map_err(|e| anyhow::anyhow!("Failed to parse: {:?}", e))?;

    // Merge inheritance maps
    let mut merged_inheritance_map = collector_result.inheritance_map;
    merged_inheritance_map.extend(dependency_inheritance_map);

    // Find and replace calls using rustpython visitor
    let mut replacer = RustPythonFunctionCallReplacer::new_with_context(
        all_replacements,
        type_context,
        file_path.to_string_lossy().into_owned(),
        module_name.to_string(),
        source.to_string(),
        merged_inheritance_map,
        builtins,
    )?;

    // Visit the AST to find replacements
    if let rustpython_ast::Mod::Module(module) = ast {
        replacer.visit_module(&module.body);
    }

    let replacements = replacer.get_replacements();

    // Apply replacements
    tracing::debug!("Applying {} replacements", replacements.len());
    for (range, replacement) in &replacements {
        if range.start < source.len() && range.end <= source.len() {
            let original = &source[range.clone()];
            tracing::debug!("Replacing '{}' with '{}'", original, replacement);
        } else {
            tracing::warn!(
                "Invalid range {:?} for source length {}",
                range,
                source.len()
            );
        }
    }
    let migrated_source = apply_replacements(source, replacements.clone());

    // Try to parse the migrated source to verify it's valid
    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);
    }

    // Update the file in type introspection context if changes were made
    if !replacements.is_empty() {
        type_context.update_file(file_path, &migrated_source)?;
    }

    Ok(migrated_source)
}

pub fn migrate_file_interactive(
    source: &str,
    module_name: &str,
    file_path: &std::path::Path,
    type_context: &mut crate::type_introspection_context::TypeIntrospectionContext,
    all_replacements: HashMap<String, ReplaceInfo>,
    inheritance_map: HashMap<String, Vec<String>>,
) -> Result<String> {
    // For now, just use non-interactive version
    migrate_file(
        source,
        module_name,
        file_path,
        type_context,
        all_replacements,
        inheritance_map,
    )
}

pub fn check_file(
    source: &str,
    module_name: &str,
    file_path: &std::path::Path,
) -> Result<CheckResult> {
    let collector = RuffDeprecatedFunctionCollector::new(module_name.to_string(), Some(file_path));
    let result = collector.collect_from_source(source.to_string())?;

    let mut check_result = CheckResult::new();

    // Add all found functions to checked_functions
    for func_name in result.replacements.keys() {
        check_result.add_checked_function(func_name.clone());
    }

    // Also add unreplaceable functions to checked_functions
    for func_name in result.unreplaceable.keys() {
        check_result.add_checked_function(func_name.clone());
    }

    // Add errors for unreplaceable functions
    for (func_name, unreplaceable) in result.unreplaceable {
        check_result.add_error(format!(
            "Function '{}' cannot be replaced: {:?}",
            func_name, unreplaceable.reason
        ));
    }

    Ok(check_result)
}