1use crate::core::types::Version;
22use crate::core::types::*;
23use anyhow::Result;
24use rustpython_ast::{self as ast};
25use rustpython_parser::{parse, Mode};
26use std::collections::{HashMap, HashSet};
27use std::path::Path;
28
29pub type ShouldRemoveCallback = Box<dyn Fn(&ReplaceInfo) -> bool>;
31
32#[derive(Debug, Clone, PartialEq)]
34pub enum OperationMode {
35 Collect,
37 RemoveWithCallback,
39}
40
41#[derive(Debug)]
43pub enum UnifiedResult {
44 Collection(CollectorResult),
45 Removal(String), }
47
48pub struct UnifiedVisitor {
50 module_name: String,
52 _file_path: Option<std::path::PathBuf>,
53 source: String,
54 class_stack: Vec<String>,
55 builtins: HashSet<String>,
56 local_classes: HashSet<String>,
57 local_functions: HashSet<String>,
58
59 replacements: HashMap<String, ReplaceInfo>,
61 unreplaceable: HashMap<String, UnreplaceableNode>,
62 imports: Vec<ImportInfo>,
63 inheritance_map: HashMap<String, Vec<String>>,
64 class_methods: HashMap<String, HashSet<String>>,
65
66 should_remove_callback: Option<ShouldRemoveCallback>,
68 lines_to_remove: Vec<(usize, usize)>,
69
70 operation_mode: OperationMode,
72}
73
74impl UnifiedVisitor {
75 pub fn new_for_collection(module_name: &str, file_path: Option<&Path>) -> Self {
77 Self::new_internal(module_name.to_string(), file_path, OperationMode::Collect)
78 }
79
80 pub fn new_for_removal_with_callback(
82 module_name: &str,
83 file_path: Option<&Path>,
84 should_remove: ShouldRemoveCallback,
85 ) -> Self {
86 let mut visitor = Self::new_internal(
87 module_name.to_string(),
88 file_path,
89 OperationMode::RemoveWithCallback,
90 );
91 visitor.should_remove_callback = Some(should_remove);
92 visitor
93 }
94
95 pub fn new_for_removal(
97 module_name: &str,
98 file_path: Option<&Path>,
99 before_version: Option<&str>,
100 remove_all: bool,
101 current_version: Option<&str>,
102 ) -> Self {
103 let before_ver = before_version.and_then(|s| s.parse::<Version>().ok());
104 let current_ver = current_version.and_then(|s| s.parse::<Version>().ok());
105
106 let should_remove = Box::new(move |replace_info: &ReplaceInfo| {
107 replace_info.should_remove(before_ver.as_ref(), remove_all, current_ver.as_ref())
108 });
109
110 Self::new_for_removal_with_callback(module_name, file_path, should_remove)
111 }
112
113 fn new_internal(
114 module_name: String,
115 file_path: Option<&Path>,
116 operation_mode: OperationMode,
117 ) -> Self {
118 Self {
119 module_name,
120 _file_path: file_path.map(Path::to_path_buf),
121 source: String::new(),
122 class_stack: Vec::new(),
123 builtins: Self::get_all_builtins(),
124 local_classes: HashSet::new(),
125 local_functions: HashSet::new(),
126
127 replacements: HashMap::new(),
128 unreplaceable: HashMap::new(),
129 imports: Vec::new(),
130 inheritance_map: HashMap::new(),
131 class_methods: HashMap::new(),
132
133 should_remove_callback: None,
134 lines_to_remove: Vec::new(),
135 operation_mode,
136 }
137 }
138
139 pub fn process_source(mut self, source: String) -> Result<UnifiedResult> {
141 let parsed = parse(&source, Mode::Module, "<module>")?;
142 self.source = source;
143
144 match parsed {
145 ast::Mod::Module(module) => {
146 for stmt in &module.body {
147 self.visit_stmt(stmt);
148 }
149 }
150 _ => {
151 }
153 }
154
155 match self.operation_mode {
156 OperationMode::Collect => Ok(UnifiedResult::Collection(CollectorResult {
157 replacements: self.replacements,
158 unreplaceable: self.unreplaceable,
159 imports: self.imports,
160 inheritance_map: self.inheritance_map,
161 class_methods: self.class_methods,
162 })),
163 OperationMode::RemoveWithCallback => {
164 let cleaned_source = self.apply_removals(&self.source);
165 Ok(UnifiedResult::Removal(cleaned_source))
166 }
167 }
168 }
169
170 fn apply_removals(&self, source: &str) -> String {
172 let mut sorted_ranges: Vec<_> = self.lines_to_remove.iter().collect();
174 sorted_ranges.sort_by_key(|(start, _)| *start);
175
176 source
177 .lines()
178 .enumerate()
179 .filter_map(|(i, line)| {
180 let should_remove = sorted_ranges
182 .binary_search_by(|&(start, end)| {
183 if i < *start {
184 std::cmp::Ordering::Greater
185 } else if i >= *end {
186 std::cmp::Ordering::Less
187 } else {
188 std::cmp::Ordering::Equal
189 }
190 })
191 .is_ok();
192
193 if should_remove {
194 None
195 } else {
196 Some(line)
197 }
198 })
199 .collect::<Vec<_>>()
200 .join("\n")
201 }
202
203 fn get_all_builtins() -> HashSet<String> {
205 use pyo3::prelude::*;
206
207 Python::with_gil(|py| {
208 let mut builtin_names = HashSet::new();
209
210 if let Ok(builtins) = py.import("builtins") {
211 if let Ok(dir_result) = builtins.dir() {
212 for item in dir_result.iter() {
213 if let Ok(name_str) = item.extract::<String>() {
214 builtin_names.insert(name_str);
215 }
216 }
217 }
218 }
219
220 builtin_names
221 })
222 }
223
224 pub fn get_builtins(&self) -> &HashSet<String> {
226 &self.builtins
227 }
228
229 fn build_full_path(&self, name: &str) -> String {
231 if self.class_stack.is_empty() {
232 format!("{}.{}", self.module_name, name)
233 } else {
234 format!(
235 "{}.{}.{}",
236 self.module_name,
237 self.class_stack.join("."),
238 name
239 )
240 }
241 }
242
243 fn has_replace_me_decorator(decorators: &[ast::Expr]) -> bool {
245 decorators.iter().any(|dec| match dec {
246 ast::Expr::Name(name) => name.id.as_str() == "replace_me",
247 ast::Expr::Call(call) => {
248 matches!(&*call.func, ast::Expr::Name(name) if name.id.as_str() == "replace_me")
249 }
250 _ => false,
251 })
252 }
253
254 fn extract_decorator_metadata(
256 &self,
257 decorators: &[ast::Expr],
258 ) -> (Option<String>, Option<String>, Option<String>) {
259 let since = Self::extract_decorator_arg(decorators, "since");
260 let remove_in = Self::extract_decorator_arg(decorators, "remove_in");
261 let message = Self::extract_decorator_arg(decorators, "message");
262 (since, remove_in, message)
263 }
264
265 fn extract_decorator_arg(decorators: &[ast::Expr], arg_name: &str) -> Option<String> {
267 decorators
268 .iter()
269 .filter_map(|dec| {
270 if let ast::Expr::Call(call) = dec {
271 if matches!(&*call.func, ast::Expr::Name(name) if name.id.as_str() == "replace_me") {
272 return call.keywords.iter().find_map(|keyword| {
273 keyword.arg.as_ref().and_then(|arg| {
274 if arg.as_str() == arg_name {
275 Self::extract_value(&keyword.value)
276 } else {
277 None
278 }
279 })
280 });
281 }
282 }
283 None
284 })
285 .next()
286 }
287
288 fn extract_value(expr: &ast::Expr) -> Option<String> {
290 match expr {
291 ast::Expr::Constant(c) => match &c.value {
292 ast::Constant::Str(s) => Some(s.to_string()),
293 ast::Constant::Int(i) => Some(i.to_string()),
294 _ => None,
295 },
296 ast::Expr::Tuple(tuple) => {
297 let parts: Vec<String> =
298 tuple.elts.iter().filter_map(Self::extract_value).collect();
299 if parts.is_empty() {
300 None
301 } else {
302 Some(parts.join("."))
303 }
304 }
305 _ => None,
306 }
307 }
308
309 fn should_process_statement(&self, decorators: &[ast::Expr]) -> bool {
311 Self::has_replace_me_decorator(decorators)
312 }
313
314 fn should_remove_replacement(&self, replace_info: &ReplaceInfo) -> bool {
316 self.should_remove_callback
317 .as_ref()
318 .is_some_and(|callback| callback(replace_info))
319 }
320
321 fn create_replace_info(
323 &self,
324 name: &str,
325 decorators: &[ast::Expr],
326 construct_type: ConstructType,
327 ) -> ReplaceInfo {
328 let full_path = self.build_full_path(name);
329 let (since, remove_in, message) = self.extract_decorator_metadata(decorators);
330
331 let mut replace_info = ReplaceInfo::new(&full_path, "", construct_type);
332 replace_info.since = since.and_then(|s| s.parse().ok());
333 replace_info.remove_in = remove_in.and_then(|s| s.parse().ok());
334 replace_info.message = message;
335
336 replace_info
337 }
338
339 fn visit_stmt(&mut self, stmt: &ast::Stmt) {
341 match stmt {
342 ast::Stmt::FunctionDef(func) => self.visit_function(func),
343 ast::Stmt::AsyncFunctionDef(func) => self.visit_async_function(func),
344 ast::Stmt::ClassDef(class) => self.visit_class(class),
345 ast::Stmt::Import(import) => self.visit_import(import),
346 ast::Stmt::ImportFrom(import) => self.visit_import_from(import),
347 ast::Stmt::Assign(assign) => self.visit_assign(assign),
348 ast::Stmt::AnnAssign(ann_assign) => self.visit_ann_assign(ann_assign),
349
350 ast::Stmt::If(if_stmt) => {
352 for stmt in &if_stmt.body {
353 self.visit_stmt(stmt);
354 }
355 for stmt in &if_stmt.orelse {
356 self.visit_stmt(stmt);
357 }
358 }
359 ast::Stmt::Try(try_stmt) => {
360 for stmt in &try_stmt.body {
361 self.visit_stmt(stmt);
362 }
363 for handler in &try_stmt.handlers {
364 let ast::ExceptHandler::ExceptHandler(h) = handler;
365 for stmt in &h.body {
366 self.visit_stmt(stmt);
367 }
368 }
369 for stmt in &try_stmt.orelse {
370 self.visit_stmt(stmt);
371 }
372 for stmt in &try_stmt.finalbody {
373 self.visit_stmt(stmt);
374 }
375 }
376 ast::Stmt::While(while_stmt) => {
377 for stmt in &while_stmt.body {
378 self.visit_stmt(stmt);
379 }
380 for stmt in &while_stmt.orelse {
381 self.visit_stmt(stmt);
382 }
383 }
384 ast::Stmt::For(for_stmt) => {
385 for stmt in &for_stmt.body {
386 self.visit_stmt(stmt);
387 }
388 for stmt in &for_stmt.orelse {
389 self.visit_stmt(stmt);
390 }
391 }
392 ast::Stmt::With(with_stmt) => {
393 for stmt in &with_stmt.body {
394 self.visit_stmt(stmt);
395 }
396 }
397 _ => {}
398 }
399 }
400
401 fn visit_function(&mut self, func: &ast::StmtFunctionDef) {
402 if self.class_stack.is_empty() {
404 self.local_functions.insert(func.name.to_string());
405 }
406
407 if !self.should_process_statement(&func.decorator_list) {
408 return;
409 }
410
411 match &self.operation_mode {
412 OperationMode::Collect => {
413 self.collect_function(func);
414 }
415 OperationMode::RemoveWithCallback => {
416 let replace_info = self.create_replace_info(
417 &func.name,
418 &func.decorator_list,
419 ConstructType::Function,
420 );
421
422 if self.should_remove_replacement(&replace_info) {
423 if let Some(line_range) = self.find_function_lines(func) {
424 self.lines_to_remove.push(line_range);
425 }
426 }
427 }
428 }
429 }
430
431 fn visit_async_function(&mut self, func: &ast::StmtAsyncFunctionDef) {
432 if !self.should_process_statement(&func.decorator_list) {
433 return;
434 }
435
436 match &self.operation_mode {
437 OperationMode::Collect => {
438 self.collect_async_function(func);
439 }
440 OperationMode::RemoveWithCallback => {
441 let replace_info = self.create_replace_info(
442 &func.name,
443 &func.decorator_list,
444 ConstructType::AsyncFunction,
445 );
446
447 if self.should_remove_replacement(&replace_info) {
448 if let Some(line_range) = self.find_async_function_lines(func) {
449 self.lines_to_remove.push(line_range);
450 }
451 }
452 }
453 }
454 }
455
456 fn visit_class(&mut self, class_def: &ast::StmtClassDef) {
457 let class_name = class_def.name.to_string();
458 let full_class_name = self.build_full_path(&class_name);
459 self.local_classes.insert(class_name.clone());
460 let bases: Vec<String> = class_def
461 .bases
462 .iter()
463 .filter_map(|base| {
464 if let ast::Expr::Name(name) = base {
465 Some(format!("{}.{}", self.module_name, name.id))
466 } else {
467 None
468 }
469 })
470 .collect();
471
472 if !bases.is_empty() {
473 self.inheritance_map.insert(full_class_name.clone(), bases);
474 }
475
476 if self.should_process_statement(&class_def.decorator_list) {
478 match &self.operation_mode {
479 OperationMode::Collect => {
480 self.collect_class(class_def);
481 }
482 OperationMode::RemoveWithCallback => {
483 let replace_info = self.create_replace_info(
484 &class_def.name,
485 &class_def.decorator_list,
486 ConstructType::Class,
487 );
488
489 if self.should_remove_replacement(&replace_info) {
490 if let Some(line_range) = self.find_class_lines(class_def) {
491 self.lines_to_remove.push(line_range);
492 }
493 }
494 }
495 }
496 }
497
498 self.class_stack.push(class_name);
500 for stmt in &class_def.body {
501 self.visit_stmt(stmt);
502 }
503 self.class_stack.pop();
504 }
505
506 fn visit_import(&mut self, import: &ast::StmtImport) {
507 for alias in &import.names {
508 self.imports.push(ImportInfo::new(
509 alias.name.to_string(),
510 vec![(
511 alias.name.to_string(),
512 alias.asname.as_ref().map(|n| n.to_string()),
513 )],
514 ));
515 }
516 }
517
518 fn visit_import_from(&mut self, import: &ast::StmtImportFrom) {
519 let names: Vec<(String, Option<String>)> = import
520 .names
521 .iter()
522 .map(|alias| {
523 (
524 alias.name.to_string(),
525 alias.asname.as_ref().map(|n| n.to_string()),
526 )
527 })
528 .collect();
529
530 let module_name = if let Some(module) = &import.module {
531 let level = import.level.as_ref().map_or(0, |i| i.to_usize());
532 let dots = ".".repeat(level);
533 format!("{}{}", dots, module)
534 } else {
535 let level = import.level.as_ref().map_or(0, |i| i.to_usize());
536 ".".repeat(level)
537 };
538
539 self.imports.push(ImportInfo::new(module_name, names));
540 }
541
542 fn visit_assign(&mut self, assign: &ast::StmtAssign) {
543 if assign.targets.len() == 1 {
545 if let ast::Expr::Name(name) = &assign.targets[0] {
546 if let ast::Expr::Call(call) = assign.value.as_ref() {
547 if matches!(&*call.func, ast::Expr::Name(func_name) if func_name.id.as_str() == "replace_me")
548 {
549 match &self.operation_mode {
550 OperationMode::Collect => {
551 self.collect_attribute_assignment(name, call);
552 }
553 OperationMode::RemoveWithCallback => {
554 let full_name = if self.class_stack.is_empty() {
556 format!("{}.{}", self.module_name, name.id)
557 } else {
558 format!(
559 "{}.{}.{}",
560 self.module_name,
561 self.class_stack.join("."),
562 name.id
563 )
564 };
565
566 let construct_type = if self.class_stack.is_empty() {
567 ConstructType::ModuleAttribute
568 } else {
569 ConstructType::ClassAttribute
570 };
571
572 let replace_info = ReplaceInfo::new(&full_name, "", construct_type);
573
574 if self.should_remove_replacement(&replace_info) {
576 }
578 }
579 }
580 }
581 }
582 }
583 }
584 }
585
586 fn visit_ann_assign(&mut self, ann_assign: &ast::StmtAnnAssign) {
587 if let Some(value) = &ann_assign.value {
589 if let ast::Expr::Name(name) = ann_assign.target.as_ref() {
590 if let ast::Expr::Call(call) = value.as_ref() {
591 if matches!(&*call.func, ast::Expr::Name(func_name) if func_name.id.as_str() == "replace_me")
592 {
593 match &self.operation_mode {
594 OperationMode::Collect => {
595 self.collect_attribute_assignment(name, call);
596 }
597 OperationMode::RemoveWithCallback => {
598 let full_name = if self.class_stack.is_empty() {
600 format!("{}.{}", self.module_name, name.id)
601 } else {
602 format!(
603 "{}.{}.{}",
604 self.module_name,
605 self.class_stack.join("."),
606 name.id
607 )
608 };
609
610 let construct_type = if self.class_stack.is_empty() {
611 ConstructType::ModuleAttribute
612 } else {
613 ConstructType::ClassAttribute
614 };
615
616 let replace_info = ReplaceInfo::new(&full_name, "", construct_type);
617
618 if self.should_remove_replacement(&replace_info) {
620 }
622 }
623 }
624 }
625 }
626 }
627 }
628 }
629
630 fn extract_replacement_from_function(&self, func: &ast::StmtFunctionDef) -> Result<String> {
632 let body_stmts: Vec<&ast::Stmt> = func
634 .body
635 .iter()
636 .skip_while(|stmt| {
637 matches!(stmt, ast::Stmt::Expr(expr_stmt) if matches!(&*expr_stmt.value,
638 ast::Expr::Constant(c) if matches!(&c.value, ast::Constant::Str(_))))
639 })
640 .filter(|stmt| !matches!(stmt, ast::Stmt::Pass(_)))
641 .collect();
642
643 if body_stmts.is_empty() {
644 return Ok("".to_string());
646 }
647
648 if body_stmts.len() == 1 {
649 if let ast::Stmt::Return(ret_stmt) = body_stmts[0] {
651 if let Some(value) = &ret_stmt.value {
652 let param_names: Vec<String> = func
654 .args
655 .args
656 .iter()
657 .map(|arg| arg.def.arg.to_string())
658 .collect();
659
660 let param_str_refs: Vec<&str> =
662 param_names.iter().map(|s| s.as_str()).collect();
663 return self.expr_to_string_with_placeholders(value, ¶m_str_refs);
664 }
665 }
666 }
667
668 Ok("".to_string())
670 }
671
672 fn expr_to_string_with_placeholders(
674 &self,
675 expr: &ast::Expr,
676 param_names: &[&str],
677 ) -> Result<String> {
678 match expr {
679 ast::Expr::Name(name) => {
680 if param_names.contains(&name.id.as_str()) {
681 Ok(format!("{{{}}}", name.id))
682 } else {
683 Ok(name.id.to_string())
684 }
685 }
686 ast::Expr::Call(call) => {
687 let func_str = self.expr_to_string_with_placeholders(&call.func, param_names)?;
688 let mut all_args: Vec<String> = Vec::new();
689
690 for arg in &call.args {
692 all_args.push(self.expr_to_string_with_placeholders(arg, param_names)?);
693 }
694
695 for keyword in &call.keywords {
697 if let Some(arg_name) = &keyword.arg {
698 let value_str = self.expr_to_string_with_placeholders(&keyword.value, param_names)?;
699 all_args.push(format!("{}={}", arg_name, value_str));
700 } else {
701 let value_str = self.expr_to_string_with_placeholders(&keyword.value, param_names)?;
703 all_args.push(format!("**{}", value_str));
704 }
705 }
706
707 Ok(format!("{}({})", func_str, all_args.join(", ")))
708 }
709 ast::Expr::Attribute(attr) => {
710 let value_str = self.expr_to_string_with_placeholders(&attr.value, param_names)?;
711 Ok(format!("{}.{}", value_str, attr.attr))
712 }
713 ast::Expr::BinOp(binop) => {
714 let left = self.expr_to_string_with_placeholders(&binop.left, param_names)?;
715 let right = self.expr_to_string_with_placeholders(&binop.right, param_names)?;
716 let op_str = match &binop.op {
717 ast::Operator::Add => "+",
718 ast::Operator::Sub => "-",
719 ast::Operator::Mult => "*",
720 ast::Operator::Div => "/",
721 ast::Operator::Mod => "%",
722 ast::Operator::Pow => "**",
723 ast::Operator::LShift => "<<",
724 ast::Operator::RShift => ">>",
725 ast::Operator::BitOr => "|",
726 ast::Operator::BitXor => "^",
727 ast::Operator::BitAnd => "&",
728 ast::Operator::FloorDiv => "//",
729 ast::Operator::MatMult => "@",
730 };
731 Ok(format!("{} {} {}", left, op_str, right))
732 }
733 ast::Expr::UnaryOp(unaryop) => {
734 let operand = self.expr_to_string_with_placeholders(&unaryop.operand, param_names)?;
735 let op_str = match &unaryop.op {
736 ast::UnaryOp::Not => "not ",
737 ast::UnaryOp::UAdd => "+",
738 ast::UnaryOp::USub => "-",
739 ast::UnaryOp::Invert => "~",
740 };
741 Ok(format!("{}{}", op_str, operand))
742 }
743 ast::Expr::List(list) => {
744 let mut elts = Vec::new();
745 for e in &list.elts {
746 elts.push(self.expr_to_string_with_placeholders(e, param_names)?);
747 }
748 Ok(format!("[{}]", elts.join(", ")))
749 }
750 ast::Expr::Tuple(tuple) => {
751 let mut elts = Vec::new();
752 for e in &tuple.elts {
753 elts.push(self.expr_to_string_with_placeholders(e, param_names)?);
754 }
755 if elts.len() == 1 {
756 Ok(format!("({},)", elts[0]))
757 } else {
758 Ok(format!("({})", elts.join(", ")))
759 }
760 }
761 ast::Expr::Dict(dict) => {
762 let mut items = Vec::new();
763 for (k, v) in dict.keys.iter().zip(&dict.values) {
764 if let Some(key) = k {
765 let key_str = self.expr_to_string_with_placeholders(key, param_names)?;
766 let val_str = self.expr_to_string_with_placeholders(v, param_names)?;
767 items.push(format!("{}: {}", key_str, val_str));
768 }
769 }
770 Ok(format!("{{{}}}", items.join(", ")))
771 }
772 ast::Expr::Constant(c) => match &c.value {
773 ast::Constant::Str(s) => Ok(format!("\"{}\"", s.escape_default())),
774 ast::Constant::Int(i) => Ok(i.to_string()),
775 ast::Constant::Float(f) => Ok(f.to_string()),
776 ast::Constant::Bool(b) => Ok(if *b { "True" } else { "False" }.to_string()),
777 ast::Constant::None => Ok("None".to_string()),
778 ast::Constant::Ellipsis => Ok("...".to_string()),
779 _ => {
780 Err(anyhow::anyhow!("Unsupported constant type in replacement expression"))
781 }
782 },
783 ast::Expr::Starred(starred) => {
784 let value = self.expr_to_string_with_placeholders(&starred.value, param_names)?;
785 Ok(format!("*{}", value))
786 }
787 ast::Expr::Subscript(sub) => {
788 let value = self.expr_to_string_with_placeholders(&sub.value, param_names)?;
789 let slice = self.expr_to_string_with_placeholders(&sub.slice, param_names)?;
790 Ok(format!("{}[{}]", value, slice))
791 }
792 _ => {
794 Err(anyhow::anyhow!(
795 "Replacement expression contains unsupported construct (e.g., comprehensions, conditionals, comparisons). Skipping function."
796 ))
797 }
798 }
799 }
800
801 fn collect_function(&mut self, func: &ast::StmtFunctionDef) {
803 let full_path = self.build_full_path(&func.name);
804 let (since, remove_in, message) = self.extract_decorator_metadata(&func.decorator_list);
805
806 let construct_type = if self.class_stack.is_empty() {
808 ConstructType::Function
809 } else {
810 let decorator_names: Vec<&str> = func
812 .decorator_list
813 .iter()
814 .filter_map(|dec| {
815 if let ast::Expr::Name(name) = dec {
816 Some(name.id.as_str())
817 } else {
818 None
819 }
820 })
821 .collect();
822
823 if decorator_names.contains(&"property") {
824 ConstructType::Property
825 } else if decorator_names.contains(&"classmethod") {
826 ConstructType::ClassMethod
827 } else if decorator_names.contains(&"staticmethod") {
828 ConstructType::StaticMethod
829 } else {
830 ConstructType::Function
831 }
832 };
833
834 let replacement_expr = match self.extract_replacement_from_function(func) {
836 Ok(expr) => expr,
837 Err(e) => {
838 tracing::warn!(
840 "Skipping function '{}' in {}: {}",
841 full_path,
842 self._file_path
843 .as_ref()
844 .and_then(|p| p.to_str())
845 .unwrap_or("<unknown>"),
846 e
847 );
848 return;
850 }
851 };
852
853 let mut replace_info = ReplaceInfo::new(&full_path, &replacement_expr, construct_type);
855 replace_info.since = since.and_then(|s| s.parse().ok());
856 replace_info.remove_in = remove_in.and_then(|s| s.parse().ok());
857 replace_info.message = message;
858
859 self.replacements.insert(full_path, replace_info);
860 }
861
862 fn collect_async_function(&mut self, func: &ast::StmtAsyncFunctionDef) {
863 let full_path = self.build_full_path(&func.name);
864 let (since, remove_in, message) = self.extract_decorator_metadata(&func.decorator_list);
865
866 let mut replace_info = ReplaceInfo::new(&full_path, "", ConstructType::AsyncFunction);
867 replace_info.since = since.and_then(|s| s.parse().ok());
868 replace_info.remove_in = remove_in.and_then(|s| s.parse().ok());
869 replace_info.message = message;
870
871 self.replacements.insert(full_path, replace_info);
872 }
873
874 fn collect_class(&mut self, class_def: &ast::StmtClassDef) {
875 let full_path = self.build_full_path(&class_def.name);
876 let (since, remove_in, message) =
877 self.extract_decorator_metadata(&class_def.decorator_list);
878
879 let mut replace_info = ReplaceInfo::new(&full_path, "", ConstructType::Class);
880 replace_info.since = since.and_then(|s| s.parse().ok());
881 replace_info.remove_in = remove_in.and_then(|s| s.parse().ok());
882 replace_info.message = message;
883
884 self.replacements.insert(full_path, replace_info);
885 }
886
887 fn collect_attribute_assignment(&mut self, name: &ast::ExprName, call: &ast::ExprCall) {
888 if let Some(arg) = call.args.first() {
889 let replacement_expr = format!("{:?}", arg); let full_name = if self.class_stack.is_empty() {
893 format!("{}.{}", self.module_name, name.id)
894 } else {
895 format!(
896 "{}.{}.{}",
897 self.module_name,
898 self.class_stack.join("."),
899 name.id
900 )
901 };
902
903 let construct_type = if self.class_stack.is_empty() {
904 ConstructType::ModuleAttribute
905 } else {
906 ConstructType::ClassAttribute
907 };
908
909 let replace_info = ReplaceInfo::new(&full_name, &replacement_expr, construct_type);
910 self.replacements.insert(full_name, replace_info);
911 }
912 }
913
914 fn find_function_lines(&self, func: &ast::StmtFunctionDef) -> Option<(usize, usize)> {
916 self.find_statement_lines_by_name("def", &func.name)
917 }
918
919 fn find_async_function_lines(
920 &self,
921 func: &ast::StmtAsyncFunctionDef,
922 ) -> Option<(usize, usize)> {
923 self.find_statement_lines_by_name("async def", &func.name)
924 }
925
926 fn find_class_lines(&self, class_def: &ast::StmtClassDef) -> Option<(usize, usize)> {
927 self.find_statement_lines_by_name("class", &class_def.name)
928 }
929
930 fn find_statement_lines_by_name(&self, keyword: &str, name: &str) -> Option<(usize, usize)> {
931 let lines: Vec<&str> = self.source.lines().collect();
932
933 for (i, line) in lines.iter().enumerate() {
934 if line.contains(&format!("{} {}", keyword, name)) {
935 let indent = line.chars().take_while(|c| c.is_whitespace()).count();
937 for (j, end_line) in lines[i + 1..].iter().enumerate() {
938 let end_i = i + j + 1;
939 if !end_line.trim().is_empty() {
940 let end_indent = end_line.chars().take_while(|c| c.is_whitespace()).count();
941 if end_indent <= indent && !end_line.trim_start().starts_with('#') {
942 let start = self.find_decorator_start(&lines, i);
944 return Some((start, end_i));
945 }
946 }
947 }
948 let start = self.find_decorator_start(&lines, i);
950 return Some((start, lines.len()));
951 }
952 }
953 None
954 }
955
956 fn find_decorator_start(&self, lines: &[&str], def_line: usize) -> usize {
957 let mut start = def_line;
959 for i in (0..def_line).rev() {
960 let line = lines[i].trim();
961 if line.starts_with('@') || line.is_empty() || line.starts_with('#') {
962 start = i;
963 } else {
964 break;
965 }
966 }
967 start
968 }
969}
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974
975 #[test]
976 fn test_unified_visitor_collection() {
977 let source = r#"
978from dissolve import replace_me
979
980@replace_me(since="1.0.0")
981def old_function():
982 return new_function()
983
984def regular_function():
985 return 42
986"#;
987
988 let visitor = UnifiedVisitor::new_for_collection("test_module", None);
989 let result = visitor.process_source(source.to_string()).unwrap();
990
991 match result {
992 UnifiedResult::Collection(collection) => {
993 assert!(collection
994 .replacements
995 .contains_key("test_module.old_function"));
996 assert!(!collection
997 .replacements
998 .contains_key("test_module.regular_function"));
999 }
1000 _ => panic!("Expected Collection result"),
1001 }
1002 }
1003
1004 #[test]
1005 fn test_unified_visitor_removal_criteria() {
1006 use crate::core::types::Version;
1007
1008 let should_remove_callback = Box::new(|replace_info: &ReplaceInfo| {
1010 if let Some(since) = &replace_info.since {
1011 let before_version = Version::new(2, 0, 0);
1012 since < &before_version
1013 } else {
1014 false
1015 }
1016 });
1017
1018 let visitor = UnifiedVisitor::new_for_removal_with_callback(
1019 "test_module",
1020 None,
1021 should_remove_callback,
1022 );
1023
1024 let replace_info = ReplaceInfo::new("test.func", "new_func()", ConstructType::Function)
1026 .with_since_version(Version::new(1, 5, 0));
1027
1028 assert!(visitor.should_remove_replacement(&replace_info));
1029
1030 let replace_info2 = ReplaceInfo::new("test.func2", "new_func2()", ConstructType::Function)
1032 .with_since_version(Version::new(2, 1, 0));
1033
1034 assert!(!visitor.should_remove_replacement(&replace_info2));
1035 }
1036
1037 #[test]
1038 fn test_unified_visitor_removal() {
1039 let source = r#"
1040from dissolve import replace_me
1041
1042@replace_me(since="1.0.0")
1043def old_function():
1044 return new_function()
1045
1046def regular_function():
1047 return 42
1048
1049@replace_me(since="2.0.0")
1050def newer_function():
1051 return new_api()
1052"#;
1053
1054 let visitor = UnifiedVisitor::new_for_removal(
1055 "test_module",
1056 None,
1057 Some("1.5.0"), false,
1059 None,
1060 );
1061
1062 let result = visitor.process_source(source.to_string()).unwrap();
1063
1064 match result {
1065 UnifiedResult::Removal(cleaned_source) => {
1066 assert!(!cleaned_source.contains("def old_function"));
1068 assert!(cleaned_source.contains("def regular_function"));
1070 assert!(cleaned_source.contains("def newer_function"));
1072 }
1073 _ => panic!("Expected Removal result"),
1074 }
1075 }
1076}