use crate::core::{ConstructType, ReplaceInfo};
use crate::migrate_ruff::migrate_file;
use crate::stub_collector::RuffDeprecatedFunctionCollector;
use crate::tests::test_utils::TestContext;
use crate::types::TypeIntrospectionMethod;
use std::collections::HashMap;
use std::path::Path;
#[test]
fn test_basic_increment_example() {
let source = r#"
from dissolve import replace_me
def increment(x):
return x + 1
@replace_me(since="0.1.0")
def inc(x):
return increment(x)
# Usage that should be migrated
result = inc(x=3)
value = inc(42)
"#;
let collector = RuffDeprecatedFunctionCollector::new("test_module".to_string(), None);
let result = collector.collect_from_source(source.to_string()).unwrap();
assert!(result.replacements.contains_key("test_module.inc"));
let replacement = &result.replacements["test_module.inc"];
assert_eq!(replacement.replacement_expr, "increment({x})");
let test_ctx = crate::tests::test_utils::TestContext::new(source);
let mut type_context = test_ctx.create_type_context(TypeIntrospectionMethod::PyrightLsp);
let migrated = migrate_file(
source,
"test_module",
Path::new(&test_ctx.file_path),
&mut type_context,
result.replacements,
HashMap::new(),
)
.unwrap();
type_context.shutdown().unwrap();
assert!(migrated.contains("result = increment(3)"));
assert!(migrated.contains("value = increment(42)"));
}
#[test]
fn test_dataprocessor_classmethod_example() {
let source = r#"
from dissolve import replace_me
class DataProcessor:
@classmethod
@replace_me(since="2.0.0")
def old_process_data(cls, data):
return cls.new_process_data(data.strip().upper())
@classmethod
def new_process_data(cls, processed_data):
return f"Processed: {processed_data}"
@staticmethod
@replace_me(since="2.0.0")
def old_utility_func(value):
return new_utility_func(value * 10)
def new_utility_func(val):
return f"Utility: {val}"
# Usage that should be migrated
result1 = DataProcessor.old_process_data(" hello ")
result2 = DataProcessor.old_utility_func(5)
"#;
let collector = RuffDeprecatedFunctionCollector::new("test_module".to_string(), None);
let result = collector.collect_from_source(source.to_string()).unwrap();
assert!(result
.replacements
.contains_key("test_module.DataProcessor.old_process_data"));
assert!(result
.replacements
.contains_key("test_module.DataProcessor.old_utility_func"));
let class_method_replacement =
&result.replacements["test_module.DataProcessor.old_process_data"];
assert_eq!(
class_method_replacement.replacement_expr,
"{cls}.new_process_data({data}.strip().upper())"
);
assert_eq!(
class_method_replacement.construct_type,
ConstructType::ClassMethod
);
let static_method_replacement =
&result.replacements["test_module.DataProcessor.old_utility_func"];
assert_eq!(
static_method_replacement.replacement_expr,
"new_utility_func({value} * 10)"
);
assert_eq!(
static_method_replacement.construct_type,
ConstructType::StaticMethod
);
let test_ctx = crate::tests::test_utils::TestContext::new(source);
let mut type_context = test_ctx.create_type_context(TypeIntrospectionMethod::PyrightLsp);
let migrated = migrate_file(
source,
"test_module",
Path::new(&test_ctx.file_path),
&mut type_context,
result.replacements,
HashMap::new(),
)
.unwrap();
type_context.shutdown().unwrap();
assert!(migrated
.contains("result1 = DataProcessor.new_process_data(\" hello \".strip().upper())"));
assert!(migrated.contains("result2 = new_utility_func(5 * 10)"));
}
#[test]
fn test_async_function_example() {
let source = r#"
from dissolve import replace_me
import asyncio
async def new_fetch_data(url, timeout=30):
# Modern implementation
return await fetch_with_timeout(url, timeout)
@replace_me(since="3.0.0")
async def old_fetch_data(url):
return await new_fetch_data(url, timeout=30)
async def fetch_with_timeout(url, timeout):
await asyncio.sleep(0.1) # Simulate network call
return {"url": url, "timeout": timeout, "data": "fetched"}
# Usage that should be migrated
async def main():
result = await old_fetch_data("https://api.example.com")
return result
"#;
let collector = RuffDeprecatedFunctionCollector::new("test_module".to_string(), None);
let result = collector.collect_from_source(source.to_string()).unwrap();
assert!(result
.replacements
.contains_key("test_module.old_fetch_data"));
let replacement = &result.replacements["test_module.old_fetch_data"];
assert_eq!(
replacement.replacement_expr,
"new_fetch_data({url}, timeout=30)"
);
assert_eq!(replacement.construct_type, ConstructType::Function);
let test_ctx = crate::tests::test_utils::TestContext::new(source);
let mut type_context = test_ctx.create_type_context(TypeIntrospectionMethod::PyrightLsp);
let migrated = migrate_file(
source,
"test_module",
Path::new(&test_ctx.file_path),
&mut type_context,
result.replacements,
HashMap::new(),
)
.unwrap();
type_context.shutdown().unwrap();
assert!(
migrated.contains("result = await new_fetch_data(\"https://api.example.com\", timeout=30)")
);
}
#[test]
fn test_fallback_implementation_behavior() {
let source_with_fallback = r#"
# Fallback implementation like in README
try:
from dissolve import replace_me
except ModuleNotFoundError:
import warnings
def replace_me(since=None, remove_in=None, message=None):
def decorator(func):
def wrapper(*args, **kwargs):
msg = f"{func.__name__} has been deprecated"
if since:
msg += f" since {since}"
if remove_in:
msg += f" and will be removed in {remove_in}"
if message:
msg += f". {message}"
else:
msg += ". Consider running 'dissolve migrate' to automatically update your code."
warnings.warn(msg, DeprecationWarning, stacklevel=2)
return func(*args, **kwargs)
return wrapper
return decorator
def new_function(value):
return value * 2
@replace_me(since="1.0.0", remove_in="2.0.0")
def old_function(value):
return new_function(value)
# Usage
result = old_function(5)
"#;
let collector = RuffDeprecatedFunctionCollector::new("test_module".to_string(), None);
let result = collector
.collect_from_source(source_with_fallback.to_string())
.unwrap();
assert!(result.replacements.contains_key("test_module.old_function"));
let replacement = &result.replacements["test_module.old_function"];
assert_eq!(replacement.replacement_expr, "new_function({value})");
let test_ctx = crate::tests::test_utils::TestContext::new(source_with_fallback);
let mut type_context = test_ctx.create_type_context(TypeIntrospectionMethod::PyrightLsp);
let migrated = migrate_file(
source_with_fallback,
"test_module",
Path::new(&test_ctx.file_path),
&mut type_context,
result.replacements,
HashMap::new(),
)
.unwrap();
type_context.shutdown().unwrap();
assert!(migrated.contains("result = new_function(5)"));
assert!(migrated.contains("try:"));
assert!(migrated.contains("from dissolve import replace_me"));
assert!(migrated.contains("except ModuleNotFoundError:"));
}
#[test]
fn test_simple_inheritance_example() {
let source = r#"
from dissolve import replace_me
class Base:
@replace_me(since="1.0.0")
def old_method(self):
return self.new_method()
def new_method(self):
return "new implementation"
# Basic test - just verify the method is detected and has correct replacement
"#;
let collector = RuffDeprecatedFunctionCollector::new("test_module".to_string(), None);
let result = collector.collect_from_source(source.to_string()).unwrap();
assert!(result
.replacements
.contains_key("test_module.Base.old_method"));
let replacement = &result.replacements["test_module.Base.old_method"];
assert_eq!(replacement.replacement_expr, "{self}.new_method()");
assert_eq!(replacement.construct_type, ConstructType::Function);
}
#[test]
fn test_attribute_deprecation_pattern() {
let source = r#"
from dissolve import replace_me
# Module-level attribute
OLD_API_URL = replace_me("https://api.example.com/v2")
# Class attribute
class Config:
OLD_TIMEOUT = replace_me(30)
OLD_DEBUG_MODE = replace_me(True)
"#;
let collector = RuffDeprecatedFunctionCollector::new("test_module".to_string(), None);
let result = collector.collect_from_source(source.to_string()).unwrap();
assert!(!result.replacements.is_empty());
assert!(result.replacements.contains_key("test_module.OLD_API_URL"));
assert!(result
.replacements
.contains_key("test_module.Config.OLD_TIMEOUT"));
assert!(result
.replacements
.contains_key("test_module.Config.OLD_DEBUG_MODE"));
}
#[test]
fn test_context_manager_example() {
let source = r#"
from dissolve import replace_me
class Repo:
@replace_me(since="1.0.0")
def stage(self, files):
return self.add_files(files)
def add_files(self, files):
return f"Added {files}"
def open_repo():
return Repo()
# Context manager usage - dissolve should track that r is a Repo
with open_repo() as r:
result = r.stage(["file1.py", "file2.py"])
"#;
let collector = RuffDeprecatedFunctionCollector::new("test_module".to_string(), None);
let result = collector.collect_from_source(source.to_string()).unwrap();
assert!(result.replacements.contains_key("test_module.Repo.stage"));
let test_ctx = crate::tests::test_utils::TestContext::new(source);
let mut type_context = test_ctx.create_type_context(TypeIntrospectionMethod::PyrightLsp);
let migrated = migrate_file(
source,
"test_module",
Path::new(&test_ctx.file_path),
&mut type_context,
result.replacements,
HashMap::new(),
)
.unwrap();
type_context.shutdown().unwrap();
assert!(migrated.contains("def stage")); }
#[test]
fn test_inheritance_example() {
let source = r#"
from dissolve import replace_me
class Base:
@replace_me(since="1.0.0")
def old_method(self):
return self.new_method()
def new_method(self):
return "new implementation"
class Child(Base):
pass
# Should migrate even though method is defined in parent class
obj = Child()
result = obj.old_method()
"#;
let collector = RuffDeprecatedFunctionCollector::new("test_module".to_string(), None);
let result = collector.collect_from_source(source.to_string()).unwrap();
assert!(result
.replacements
.contains_key("test_module.Base.old_method"));
assert!(result.inheritance_map.contains_key("test_module.Child"));
assert_eq!(
result.inheritance_map["test_module.Child"],
vec!["test_module.Base"]
);
let test_ctx = crate::tests::test_utils::TestContext::new(source);
let mut type_context = test_ctx.create_type_context(TypeIntrospectionMethod::PyrightLsp);
let migrated = migrate_file(
source,
"test_module",
Path::new(&test_ctx.file_path),
&mut type_context,
result.replacements,
result.inheritance_map,
)
.unwrap();
type_context.shutdown().unwrap();
assert!(migrated.contains("class Child"));
assert!(migrated.contains("obj = Child()"));
}
#[test]
fn test_import_resolution_patterns() {
let source = r#"
from dissolve import replace_me
def new_function():
return "new implementation"
@replace_me(since="1.0.0")
def old_function():
return new_function()
# Different import patterns that should all be handled
from mylib.utils import old_function
from mylib.utils import old_function as legacy_func
import mylib.utils
# These should all be recognized and migrated
result1 = old_function() # Direct import
result2 = legacy_func() # Aliased import
result3 = mylib.utils.old_function() # Module attribute access
"#;
let mut replacements = HashMap::new();
let mut replacement_info = ReplaceInfo::new(
"mylib.utils.old_function",
"new_function()",
ConstructType::Function,
);
if let Ok(rustpython_ast::Mod::Module(module)) = rustpython_parser::parse(
"def old_function(): return new_function()",
rustpython_parser::Mode::Module,
"<test>",
) {
if let Some(rustpython_ast::Stmt::FunctionDef(func)) = module.body.first() {
if let Some(rustpython_ast::Stmt::Return(ret)) = func.body.first() {
replacement_info.replacement_ast = ret.value.clone();
}
}
}
replacements.insert("mylib.utils.old_function".to_string(), replacement_info);
let test_ctx = TestContext::new(source);
let mut type_context = test_ctx.create_type_context(TypeIntrospectionMethod::PyrightLsp);
let result = migrate_file(
source,
"test_module",
Path::new(&test_ctx.file_path),
&mut type_context,
replacements,
HashMap::new(),
)
.unwrap();
type_context.shutdown().unwrap();
assert!(result.contains("from mylib.utils import old_function"));
assert!(result.contains("import mylib.utils"));
}
#[test]
fn test_import_resolution_compatibility() {
let source = r#"
# Different import styles
from dissolve import replace_me
def new_function():
return "new implementation"
@replace_me(since="1.0.0")
def old_function():
return new_function()
"#;
let collector = RuffDeprecatedFunctionCollector::new("test_module".to_string(), None);
let result = collector.collect_from_source(source.to_string()).unwrap();
assert!(result.replacements.contains_key("test_module.old_function"));
let replacement = &result.replacements["test_module.old_function"];
assert_eq!(replacement.replacement_expr, "new_function()");
assert!(result
.imports
.iter()
.any(|import| import.module == "dissolve"));
}