Skip to main content

dissolve_python/
unified_visitor.rs

1// Copyright (C) 2024 Jelmer Vernooij <jelmer@samba.org>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Unified AST visitor for deprecated function collection and cleanup operations
16//!
17//! This visitor consolidates the shared logic between collection ('dissolve info')
18//! and cleanup ('dissolve cleanup') operations. Migration continues to use the
19//! specialized rustpython_visitor approach due to its complexity.
20
21use 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
29/// Callback type for determining if a replacement should be removed
30pub type ShouldRemoveCallback = Box<dyn Fn(&ReplaceInfo) -> bool>;
31
32/// Operation mode for the unified visitor
33#[derive(Debug, Clone, PartialEq)]
34pub enum OperationMode {
35    /// Collect deprecated functions and metadata
36    Collect,
37    /// Remove deprecated functions using a callback
38    RemoveWithCallback,
39}
40
41/// Result type for unified operations
42#[derive(Debug)]
43pub enum UnifiedResult {
44    Collection(CollectorResult),
45    Removal(String), // cleaned source
46}
47
48/// Unified visitor that handles collection and cleanup operations
49pub struct UnifiedVisitor {
50    // Common fields
51    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    // Collection fields
60    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    // Removal fields (when in remove mode)
67    should_remove_callback: Option<ShouldRemoveCallback>,
68    lines_to_remove: Vec<(usize, usize)>,
69
70    // Operation mode
71    operation_mode: OperationMode,
72}
73
74impl UnifiedVisitor {
75    /// Create a new unified visitor for collection
76    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    /// Create a new unified visitor for removal with a callback
81    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    /// Create a new unified visitor for removal with version criteria
96    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    /// Process source code according to the operation mode
140    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                // Handle other module types if needed
152            }
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    /// Apply line removals to source code
171    fn apply_removals(&self, source: &str) -> String {
172        // Sort removal ranges by start position for efficient processing
173        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                // Check if line should be removed using binary search for efficiency
181                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    /// Get all builtin names from Python
204    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    /// Get the builtins set
225    pub fn get_builtins(&self) -> &HashSet<String> {
226        &self.builtins
227    }
228
229    /// Build the full object path including module and class names
230    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    /// Check if a decorator list contains @replace_me
244    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    /// Extract decorator metadata (since, remove_in, message)
255    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    /// Extract any argument from @replace_me decorator
266    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    /// Extract string value from AST expression
289    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    /// Check if statement should be processed based on operation mode
310    fn should_process_statement(&self, decorators: &[ast::Expr]) -> bool {
311        Self::has_replace_me_decorator(decorators)
312    }
313
314    /// Check if a replacement should be removed using the callback
315    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    /// Create ReplaceInfo from decorator metadata
322    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    /// Visit a statement in the AST
340    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            // Handle compound statements - visit their bodies
351            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        // Track locally defined functions
403        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        // Check if class has @replace_me
477        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        // Visit class body
499        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        // Handle module/class attribute assignments with replace_me
544        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                                // Create a basic ReplaceInfo for the assignment
555                                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                                // Check if it should be removed - for now, assignment removal not implemented
575                                if self.should_remove_replacement(&replace_info) {
576                                    // TODO: Find assignment line range for removal
577                                }
578                            }
579                        }
580                    }
581                }
582            }
583        }
584    }
585
586    fn visit_ann_assign(&mut self, ann_assign: &ast::StmtAnnAssign) {
587        // Handle annotated assignments with replace_me
588        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                                // Create a basic ReplaceInfo for the assignment
599                                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                                // Check if it should be removed - for now, assignment removal not implemented
619                                if self.should_remove_replacement(&replace_info) {
620                                    // TODO: Find assignment line range for removal
621                                }
622                            }
623                        }
624                    }
625                }
626            }
627        }
628    }
629
630    /// Extract replacement expression from function body
631    fn extract_replacement_from_function(&self, func: &ast::StmtFunctionDef) -> Result<String> {
632        // Skip docstring and pass statements
633        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            // Empty body means remove the function completely
645            return Ok("".to_string());
646        }
647
648        if body_stmts.len() == 1 {
649            // Single statement - should be a return statement
650            if let ast::Stmt::Return(ret_stmt) = body_stmts[0] {
651                if let Some(value) = &ret_stmt.value {
652                    // Get function parameter names
653                    let param_names: Vec<String> = func
654                        .args
655                        .args
656                        .iter()
657                        .map(|arg| arg.def.arg.to_string())
658                        .collect();
659
660                    // Convert expression to string with parameter placeholders
661                    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, &param_str_refs);
664                }
665            }
666        }
667
668        // Multiple statements or no clear replacement
669        Ok("".to_string())
670    }
671
672    /// Convert expression to string with parameter placeholders
673    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                // Add positional arguments
691                for arg in &call.args {
692                    all_args.push(self.expr_to_string_with_placeholders(arg, param_names)?);
693                }
694
695                // Add keyword arguments
696                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                        // **kwargs expansion
702                        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            // Complex expressions that we don't support
793            _ => {
794                Err(anyhow::anyhow!(
795                    "Replacement expression contains unsupported construct (e.g., comprehensions, conditionals, comparisons). Skipping function."
796                ))
797            }
798        }
799    }
800
801    // Collection methods (simplified from stub_collector.rs)
802    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        // Determine construct type
807        let construct_type = if self.class_stack.is_empty() {
808            ConstructType::Function
809        } else {
810            // Check for method decorators
811            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        // Extract replacement from function body
835        let replacement_expr = match self.extract_replacement_from_function(func) {
836            Ok(expr) => expr,
837            Err(e) => {
838                // Log warning and skip this function
839                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 early without adding to replacements
849                return;
850            }
851        };
852
853        // Create ReplaceInfo with the extracted replacement
854        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            // Simple string representation for the assignment value
890            let replacement_expr = format!("{:?}", arg); // Basic fallback for collection
891
892            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    // Line finding methods for removal (simplified from remover.rs)
915    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                // Find the end of the statement (next def, class, or dedent)
936                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                            // Include decorators before the statement
943                            let start = self.find_decorator_start(&lines, i);
944                            return Some((start, end_i));
945                        }
946                    }
947                }
948                // If we reach the end, include everything
949                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        // Look backwards for decorators
958        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        // Test the callback approach
1009        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        // Create a ReplaceInfo with since="1.5.0"
1025        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        // Test that version >= 2.0.0 is NOT removed
1031        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"), // Remove functions deprecated before 1.5.0
1058            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                // old_function (since="1.0.0") should be removed
1067                assert!(!cleaned_source.contains("def old_function"));
1068                // regular_function should remain
1069                assert!(cleaned_source.contains("def regular_function"));
1070                // newer_function (since="2.0.0") should remain
1071                assert!(cleaned_source.contains("def newer_function"));
1072            }
1073            _ => panic!("Expected Removal result"),
1074        }
1075    }
1076}