dissolve-python 0.3.0

A tool to dissolve deprecated calls in Python codebases
Documentation
// Test to compare dmypy vs pyright behavior for method migration

#[cfg(test)]
mod tests {
    use crate::core::{ConstructType, ParameterInfo, ReplaceInfo};
    use crate::migrate_ruff::migrate_file;
    use crate::type_introspection_context::TypeIntrospectionContext;
    use crate::types::TypeIntrospectionMethod;
    use std::collections::HashMap;
    use std::path::Path;

    #[test]
    fn test_method_migration_with_dmypy() {
        let source = r#"
class Calculator:
    def add(self, x: int, y: int) -> int:
        return x + y  # Valid implementation for type analysis
    
    def add_numbers(self, x: int, y: int) -> int:
        return x + y  # Target method exists

calc = Calculator()
result = calc.add(5, 3)
"#;

        // Create replacement info
        let mut replacements = HashMap::new();
        replacements.insert(
            "test_module.Calculator.add".to_string(),
            ReplaceInfo {
                old_name: "add".to_string(),
                replacement_expr: "{self}.add_numbers({x}, {y})".to_string(),
                replacement_ast: None,
                construct_type: ConstructType::Function,
                parameters: vec![
                    ParameterInfo::new("self"),
                    ParameterInfo::new("x"),
                    ParameterInfo::new("y"),
                ],
                return_type: None,
                since: None,
                remove_in: None,
                message: None,
            },
        );

        let test_ctx = crate::tests::test_utils::TestContext::new(source);
        let workspace_root = std::path::Path::new(&test_ctx.file_path)
            .parent()
            .unwrap()
            .to_str()
            .unwrap();
        let mut type_context = TypeIntrospectionContext::new_with_workspace(
            TypeIntrospectionMethod::MypyDaemon,
            Some(workspace_root),
        )
        .unwrap();
        let migrated = migrate_file(
            source,
            "test_module",
            Path::new(&test_ctx.file_path),
            &mut type_context,
            replacements,
            HashMap::new(),
        )
        .unwrap();

        println!("dmypy migration result:\n{}", migrated);
        assert!(migrated.contains("result = calc.add_numbers(5, 3)"));
        assert!(!migrated.contains("result = calc.add(5, 3)"));

        // Cleanup
        type_context.shutdown().unwrap();
    }

    #[test]
    fn test_method_migration_with_pyright_for_comparison() {
        let source = r#"
from dissolve import replace_me

class Calculator:
    @replace_me()
    def add(self, x, y):
        return self.add_numbers(x, y)

calc = Calculator()
result = calc.add(5, 3)
"#;

        // Create replacement info
        let mut replacements = HashMap::new();
        replacements.insert(
            "test_module.Calculator.add".to_string(),
            ReplaceInfo {
                old_name: "add".to_string(),
                replacement_expr: "{self}.add_numbers({x}, {y})".to_string(),
                replacement_ast: None,
                construct_type: ConstructType::Function,
                parameters: vec![
                    ParameterInfo::new("self"),
                    ParameterInfo::new("x"),
                    ParameterInfo::new("y"),
                ],
                return_type: None,
                since: None,
                remove_in: None,
                message: None,
            },
        );

        let test_ctx = crate::tests::test_utils::TestContext::new(source);
        let mut type_context =
            TypeIntrospectionContext::new(TypeIntrospectionMethod::PyrightLsp).unwrap();
        let migrated = migrate_file(
            source,
            "test_module",
            Path::new(&test_ctx.file_path),
            &mut type_context,
            replacements,
            HashMap::new(),
        )
        .unwrap();

        println!("pyright migration result:\n{}", migrated);
        assert!(migrated.contains("result = calc.add_numbers(5, 3)"));
        assert!(!migrated.contains("result = calc.add(5, 3)"));

        // Cleanup
        type_context.shutdown().unwrap();
    }

    #[test]
    fn test_mypy_type_errors_handling() {
        // This test indirectly tests the mypy error handling (mypy_lsp.rs:80:12)
        // by checking that type introspection works even with type errors in the file
        use crate::mypy_lsp::MypyTypeIntrospector;
        use std::fs;
        use tempfile::TempDir;

        // Create a temporary directory with Python code containing type errors
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test_type_errors.py");

        // Write Python code with type errors
        let source = r#"
def add(a: int, b: int) -> int:
    return a + b

# This will cause a type error
result: str = add(1, 2)  # Type error: int assigned to str

def get_value() -> str:
    return 42  # Type error: returning int instead of str
"#;

        fs::write(&file_path, source).unwrap();

        // Try to create introspector - this will internally check files
        let introspector_result =
            MypyTypeIntrospector::new(Some(&temp_dir.path().to_string_lossy()));

        // The introspector should handle type errors gracefully
        // It may succeed (dmypy continues despite errors) or fail with a proper error message
        match introspector_result {
            Ok(mut intro) => {
                // Try to get type information - this will trigger ensure_file_checked internally
                let type_result = intro.get_type_at_position(
                    &file_path.to_string_lossy(),
                    3, // Line with the 'add' function
                    4, // Column position
                );

                // Should either get type info despite errors, or fail gracefully
                match type_result {
                    Ok(_) => {} // Type info retrieved despite type errors
                    Err(e) => {
                        // Error should be meaningful, not a panic
                        assert!(!e.is_empty(), "Should have a proper error message");
                    }
                }
            }
            Err(e) => {
                // Should fail with a meaningful error, not panic
                assert!(
                    !e.to_string().is_empty(),
                    "Should have a proper error message: {}",
                    e
                );
            }
        }
    }
}