Skip to main content

dissolve_python/
stub_collector.rs

1// Implementation of RuffDeprecatedFunctionCollector using rustpython-parser
2use crate::core::types::*;
3use anyhow::Result;
4use rustpython_ast::{self as ast};
5use rustpython_parser::{parse, Mode};
6use std::collections::{HashMap, HashSet};
7use std::path::{Path, PathBuf};
8
9pub struct RuffDeprecatedFunctionCollector {
10    module_name: String,
11    _file_path: Option<PathBuf>,
12    replacements: HashMap<String, ReplaceInfo>,
13    unreplaceable: HashMap<String, UnreplaceableNode>,
14    imports: Vec<ImportInfo>,
15    inheritance_map: HashMap<String, Vec<String>>,
16    class_methods: HashMap<String, HashSet<String>>,
17    class_stack: Vec<String>,
18    source: String,
19    builtins: HashSet<String>,
20    local_classes: HashSet<String>,
21    local_functions: HashSet<String>,
22}
23
24impl RuffDeprecatedFunctionCollector {
25    pub fn new(module_name: String, file_path: Option<&Path>) -> Self {
26        Self {
27            module_name,
28            _file_path: file_path.map(Path::to_path_buf),
29            replacements: HashMap::new(),
30            unreplaceable: HashMap::new(),
31            imports: Vec::new(),
32            inheritance_map: HashMap::new(),
33            class_methods: HashMap::new(),
34            class_stack: Vec::new(),
35            source: String::new(),
36            builtins: Self::get_all_builtins(),
37            local_classes: HashSet::new(),
38            local_functions: HashSet::new(),
39        }
40    }
41
42    /// Collect from source string
43    pub fn collect_from_source(mut self, source: String) -> Result<CollectorResult> {
44        self.source = source;
45        let parsed = parse(&self.source, Mode::Module, "<module>")?;
46
47        match parsed {
48            ast::Mod::Module(module) => {
49                for stmt in &module.body {
50                    self.visit_stmt(stmt);
51                }
52            }
53            ast::Mod::Expression(_) => {
54                // Not handling expression mode
55            }
56            _ => {
57                // Not handling other modes
58            }
59        }
60
61        Ok(CollectorResult {
62            replacements: self.replacements,
63            unreplaceable: self.unreplaceable,
64            imports: self.imports,
65            inheritance_map: self.inheritance_map,
66            class_methods: self.class_methods,
67        })
68    }
69
70    /// Build the full object path including module and class names
71    fn build_full_path(&self, name: &str) -> String {
72        let mut parts = Vec::with_capacity(2 + self.class_stack.len());
73        parts.push(self.module_name.as_str());
74        parts.extend(self.class_stack.iter().map(|s| s.as_str()));
75        parts.push(name);
76        parts.join(".")
77    }
78
79    /// Build a qualified name from an expression (e.g., module.Class)
80    fn build_qualified_name_from_expr(&self, expr: &ast::Expr) -> String {
81        match expr {
82            ast::Expr::Name(name) => {
83                // Simple name - assume it's in the current module
84                format!("{}.{}", self.module_name, name.id)
85            }
86            ast::Expr::Attribute(attr) => {
87                // Handle nested attributes like a.b.c
88                let mut result = attr.attr.to_string();
89                let mut current = &*attr.value;
90
91                loop {
92                    match current {
93                        ast::Expr::Name(name) => {
94                            result = format!("{}.{}", name.id, result);
95                            break;
96                        }
97                        ast::Expr::Attribute(inner_attr) => {
98                            result = format!("{}.{}", inner_attr.attr, result);
99                            current = &*inner_attr.value;
100                        }
101                        _ => {
102                            return attr.attr.to_string();
103                        }
104                    }
105                }
106
107                result
108            }
109            _ => "Unknown".to_string(),
110        }
111    }
112
113    /// Check if a decorator list contains @replace_me
114    fn has_replace_me_decorator(decorators: &[ast::Expr]) -> bool {
115        decorators.iter().any(|dec| match dec {
116            ast::Expr::Name(name) => name.id.as_str() == "replace_me",
117            ast::Expr::Call(call) => {
118                matches!(&*call.func, ast::Expr::Name(name) if name.id.as_str() == "replace_me")
119            }
120            _ => false,
121        })
122    }
123
124    /// Extract the 'since' version from @replace_me decorator
125    fn extract_since_version(&self, decorators: &[ast::Expr]) -> Option<String> {
126        self.extract_decorator_version_arg(decorators, "since")
127    }
128
129    fn extract_remove_in_version(&self, decorators: &[ast::Expr]) -> Option<String> {
130        self.extract_decorator_version_arg(decorators, "remove_in")
131    }
132
133    fn extract_message(decorators: &[ast::Expr]) -> Option<String> {
134        Self::extract_decorator_string_arg(decorators, "message")
135    }
136
137    fn extract_decorator_version_arg(
138        &self,
139        decorators: &[ast::Expr],
140        arg_name: &str,
141    ) -> Option<String> {
142        for dec in decorators {
143            if let ast::Expr::Call(call) = dec {
144                if matches!(&*call.func, ast::Expr::Name(name) if name.id.as_str() == "replace_me")
145                {
146                    for keyword in &call.keywords {
147                        if let Some(arg) = &keyword.arg {
148                            if arg.as_str() == arg_name {
149                                match &keyword.value {
150                                    // String literal: "1.2.3"
151                                    ast::Expr::Constant(c) => {
152                                        if let ast::Constant::Str(s) = &c.value {
153                                            return Some(s.to_string());
154                                        }
155                                    }
156                                    // Tuple literal: (1, 2, 3)
157                                    ast::Expr::Tuple(tuple) => {
158                                        let parts: Vec<String> = tuple
159                                            .elts
160                                            .iter()
161                                            .filter_map(|elt| match elt {
162                                                ast::Expr::Constant(c) => match &c.value {
163                                                    ast::Constant::Int(i) => Some(i.to_string()),
164                                                    ast::Constant::Str(s) => Some(s.to_string()),
165                                                    _ => None,
166                                                },
167                                                _ => None,
168                                            })
169                                            .collect();
170                                        if !parts.is_empty() {
171                                            return Some(parts.join("."));
172                                        }
173                                    }
174                                    _ => {}
175                                }
176                            }
177                        }
178                    }
179                }
180            }
181        }
182        None
183    }
184
185    fn extract_decorator_string_arg(decorators: &[ast::Expr], arg_name: &str) -> Option<String> {
186        for dec in decorators {
187            if let ast::Expr::Call(call) = dec {
188                if matches!(&*call.func, ast::Expr::Name(name) if name.id.as_str() == "replace_me")
189                {
190                    for keyword in &call.keywords {
191                        if let Some(arg) = &keyword.arg {
192                            if arg.as_str() == arg_name {
193                                if let ast::Expr::Constant(c) = &keyword.value {
194                                    if let ast::Constant::Str(s) = &c.value {
195                                        return Some(s.to_string());
196                                    }
197                                }
198                            }
199                        }
200                    }
201                }
202            }
203        }
204        None
205    }
206
207    /// Extract replacement expression from @replace_me decorator arguments
208    fn extract_replacement_from_decorator(
209        &self,
210        decorators: &[ast::Expr],
211    ) -> Option<(String, ast::Expr)> {
212        for dec in decorators {
213            if let ast::Expr::Call(call) = dec {
214                if matches!(&*call.func, ast::Expr::Name(name) if name.id.as_str() == "replace_me")
215                {
216                    for keyword in &call.keywords {
217                        if let Some(arg) = &keyword.arg {
218                            if arg.as_str() == "replacement" {
219                                let replacement_expr = self.expr_to_string(&keyword.value);
220                                return Some((replacement_expr, keyword.value.clone()));
221                            }
222                        }
223                    }
224                }
225            }
226        }
227        None
228    }
229
230    /// Extract parameters from a function
231    fn extract_parameters(&self, func: &ast::StmtFunctionDef) -> Vec<ParameterInfo> {
232        let mut params = Vec::new();
233
234        // Regular parameters
235        for arg in &func.args.args {
236            let has_default = arg.default.is_some();
237            let default_value = arg.default.as_ref().map(|e| self.expr_to_string(e));
238
239            let mut param_info = ParameterInfo::new(&arg.def.arg);
240            param_info.has_default = has_default;
241            param_info.default_value = default_value;
242            params.push(param_info);
243        }
244
245        // *args
246        if let Some(vararg) = &func.args.vararg {
247            params.push(ParameterInfo::vararg(&vararg.arg));
248        }
249
250        // Keyword-only parameters
251        for arg in &func.args.kwonlyargs {
252            let has_default = arg.default.is_some();
253            let default_value = arg.default.as_ref().map(|e| self.expr_to_string(e));
254
255            let mut param_info = ParameterInfo::new(&arg.def.arg);
256            param_info.has_default = has_default;
257            param_info.default_value = default_value;
258            param_info.is_kwonly = true;
259            params.push(param_info);
260        }
261
262        // **kwargs
263        if let Some(kwarg) = &func.args.kwarg {
264            params.push(ParameterInfo::kwarg(&kwarg.arg));
265        }
266
267        params
268    }
269
270    /// Extract parameters from an async function
271    fn extract_async_parameters(&self, func: &ast::StmtAsyncFunctionDef) -> Vec<ParameterInfo> {
272        let mut params = Vec::new();
273
274        // Regular parameters
275        for arg in &func.args.args {
276            let has_default = arg.default.is_some();
277            let default_value = arg.default.as_ref().map(|e| self.expr_to_string(e));
278            let mut param_info = ParameterInfo::new(&arg.def.arg);
279            param_info.has_default = has_default;
280            param_info.default_value = default_value;
281            params.push(param_info);
282        }
283
284        // *args
285        if let Some(vararg) = &func.args.vararg {
286            params.push(ParameterInfo::vararg(&vararg.arg));
287        }
288
289        // Keyword-only parameters
290        for arg in &func.args.kwonlyargs {
291            let has_default = arg.default.is_some();
292            let default_value = arg.default.as_ref().map(|e| self.expr_to_string(e));
293            let mut param_info = ParameterInfo::new(&arg.def.arg);
294            param_info.has_default = has_default;
295            param_info.default_value = default_value;
296            param_info.is_kwonly = true;
297            params.push(param_info);
298        }
299
300        // **kwargs
301        if let Some(kwarg) = &func.args.kwarg {
302            params.push(ParameterInfo::kwarg(&kwarg.arg));
303        }
304
305        params
306    }
307
308    /// Extract replacement expression from function body
309    fn extract_replacement_from_function(
310        &self,
311        func: &ast::StmtFunctionDef,
312    ) -> Result<(String, ast::Expr), ReplacementExtractionError> {
313        // Skip docstring and pass statements
314        let body_stmts: Vec<&ast::Stmt> = func
315            .body
316            .iter()
317            .skip_while(|stmt| {
318                matches!(stmt, ast::Stmt::Expr(expr_stmt) if matches!(expr_stmt.value.as_ref(),
319                    ast::Expr::Constant(c) if matches!(&c.value, ast::Constant::Str(_))))
320            })
321            .filter(|stmt| !matches!(stmt, ast::Stmt::Pass(_)))
322            .collect();
323
324        if body_stmts.is_empty() {
325            // Empty body is valid - means remove the function completely
326            return Ok((
327                "".to_string(),
328                ast::Expr::Constant(ast::ExprConstant {
329                    value: ast::Constant::Str("".to_string()),
330                    kind: None,
331                    range: Default::default(),
332                }),
333            ));
334        }
335
336        if body_stmts.len() > 1 {
337            return Err(ReplacementExtractionError::new(
338                func.name.to_string(),
339                ReplacementFailureReason::MultipleStatements,
340                "Function body contains multiple statements".to_string(),
341            ));
342        }
343
344        // Extract return expression
345        match body_stmts[0] {
346            ast::Stmt::Return(ret_stmt) => {
347                if let Some(value) = &ret_stmt.value {
348                    // Get function parameters for placeholder conversion
349                    let param_names: HashSet<String> = self
350                        .extract_parameters(func)
351                        .into_iter()
352                        .filter(|p| !p.is_vararg && !p.is_kwarg)
353                        .map(|p| p.name)
354                        .collect();
355
356                    // Convert the AST expression to string with placeholders
357                    let replacement_expr =
358                        self.expr_to_string_with_placeholders(value, &param_names);
359
360                    tracing::debug!(
361                        "Extracted replacement expression: {} for function {}",
362                        replacement_expr,
363                        func.name
364                    );
365
366                    Ok((replacement_expr, (**value).clone()))
367                } else {
368                    Err(ReplacementExtractionError::new(
369                        func.name.to_string(),
370                        ReplacementFailureReason::NoReturnStatement,
371                        "Return statement has no value".to_string(),
372                    ))
373                }
374            }
375            _ => Err(ReplacementExtractionError::new(
376                func.name.to_string(),
377                ReplacementFailureReason::NoReturnStatement,
378                "Function body does not contain a return statement".to_string(),
379            )),
380        }
381    }
382
383    /// Get all builtin names from Python
384    fn get_all_builtins() -> HashSet<String> {
385        use pyo3::prelude::*;
386
387        Python::with_gil(|py| {
388            let mut builtin_names = HashSet::new();
389
390            if let Ok(builtins) = py.import("builtins") {
391                if let Ok(dir_result) = builtins.dir() {
392                    for item in dir_result.iter() {
393                        if let Ok(name_str) = item.extract::<String>() {
394                            builtin_names.insert(name_str);
395                        }
396                    }
397                }
398            }
399
400            builtin_names
401        })
402    }
403
404    /// Check if a name is imported from another module
405    fn is_imported(&self, name: &str) -> bool {
406        for import_info in &self.imports {
407            for (imported_name, alias) in &import_info.names {
408                let effective_name = alias.as_ref().unwrap_or(imported_name);
409                if effective_name == name {
410                    return true;
411                }
412            }
413        }
414        false
415    }
416
417    fn is_locally_available(&self, name: &str) -> bool {
418        self.is_imported(name)
419            || self.local_classes.contains(name)
420            || self.local_functions.contains(name)
421    }
422
423    /// Get the builtins set
424    pub fn builtins(&self) -> &HashSet<String> {
425        &self.builtins
426    }
427
428    /// Convert expression to string
429    fn expr_to_string(&self, expr: &ast::Expr) -> String {
430        // Use the full implementation to avoid "..." fallbacks
431        self.expr_to_string_with_placeholders(expr, &HashSet::new())
432    }
433
434    fn expr_to_string_with_placeholders(
435        &self,
436        expr: &ast::Expr,
437        param_names: &HashSet<String>,
438    ) -> String {
439        match expr {
440            ast::Expr::Name(name) => {
441                let name_str = name.id.to_string();
442                if param_names.contains(&name_str) {
443                    format!("{{{}}}", name_str)
444                } else {
445                    name_str
446                }
447            }
448            ast::Expr::Call(call) => {
449                let func_str = match &*call.func {
450                    ast::Expr::Name(name) => {
451                        let name_str = name.id.to_string();
452                        if param_names.contains(&name_str) {
453                            format!("{{{}}}", name_str)
454                        } else {
455                            // For class replacements, add module prefix if the called name
456                            // is not imported and not locally defined and looks like a class (starts with uppercase)
457                            if name_str.chars().next().is_some_and(|c| c.is_uppercase()) {
458                                if !self.is_locally_available(&name_str) {
459                                    format!("{}.{}", self.module_name, name_str)
460                                } else {
461                                    name_str
462                                }
463                            } else {
464                                // Don't add module prefix for lowercase names - let the migration handle it
465                                name_str
466                            }
467                        }
468                    }
469                    _ => self.expr_to_string_with_placeholders(&call.func, param_names),
470                };
471                let mut args = Vec::new();
472
473                // Handle positional arguments
474                for arg in &call.args {
475                    args.push(self.expr_to_string_with_placeholders(arg, param_names));
476                }
477
478                // Handle keyword arguments
479                for keyword in &call.keywords {
480                    if let Some(arg_name) = &keyword.arg {
481                        let value_str =
482                            self.expr_to_string_with_placeholders(&keyword.value, param_names);
483                        args.push(format!("{}={}", arg_name, value_str));
484                    } else {
485                        // **kwargs expansion
486                        let inner =
487                            self.expr_to_string_with_placeholders(&keyword.value, param_names);
488                        // Check if this is **kwargs
489                        if let ast::Expr::Name(name) = &keyword.value {
490                            if name.id.to_string() == "kwargs" {
491                                args.push("{**kwargs}".to_string());
492                                continue;
493                            }
494                        }
495                        args.push(format!("**{}", inner));
496                    }
497                }
498
499                format!("{}({})", func_str, args.join(", "))
500            }
501            ast::Expr::Attribute(attr) => {
502                let value_str = self.expr_to_string_with_placeholders(&attr.value, param_names);
503                format!("{}.{}", value_str, attr.attr)
504            }
505            ast::Expr::Starred(starred) => {
506                let inner = self.expr_to_string_with_placeholders(&starred.value, param_names);
507                // Check if this is a varargs parameter (*args)
508                if let ast::Expr::Name(name) = starred.value.as_ref() {
509                    if name.id.to_string() == "args" {
510                        return "{*args}".to_string();
511                    }
512                }
513                format!("*{}", inner)
514            }
515            ast::Expr::BinOp(binop) => {
516                let left = self.expr_to_string_with_placeholders(&binop.left, param_names);
517                let right = self.expr_to_string_with_placeholders(&binop.right, param_names);
518                let op_str = match binop.op {
519                    ast::Operator::Add => " + ",
520                    ast::Operator::Sub => " - ",
521                    ast::Operator::Mult => " * ",
522                    ast::Operator::Div => " / ",
523                    ast::Operator::Mod => " % ",
524                    ast::Operator::Pow => " ** ",
525                    ast::Operator::LShift => " << ",
526                    ast::Operator::RShift => " >> ",
527                    ast::Operator::BitOr => " | ",
528                    ast::Operator::BitXor => " ^ ",
529                    ast::Operator::BitAnd => " & ",
530                    ast::Operator::FloorDiv => " // ",
531                    ast::Operator::MatMult => " @ ",
532                };
533                format!("{}{}{}", left, op_str, right)
534            }
535            ast::Expr::Compare(compare) => {
536                let mut result = self.expr_to_string_with_placeholders(&compare.left, param_names);
537                for (op, comparator) in compare.ops.iter().zip(&compare.comparators) {
538                    let op_str = match op {
539                        ast::CmpOp::Eq => " == ",
540                        ast::CmpOp::NotEq => " != ",
541                        ast::CmpOp::Lt => " < ",
542                        ast::CmpOp::LtE => " <= ",
543                        ast::CmpOp::Gt => " > ",
544                        ast::CmpOp::GtE => " >= ",
545                        ast::CmpOp::Is => " is ",
546                        ast::CmpOp::IsNot => " is not ",
547                        ast::CmpOp::In => " in ",
548                        ast::CmpOp::NotIn => " not in ",
549                    };
550                    result.push_str(op_str);
551                    result
552                        .push_str(&self.expr_to_string_with_placeholders(comparator, param_names));
553                }
554                result
555            }
556            ast::Expr::BoolOp(boolop) => {
557                let op_str = match boolop.op {
558                    ast::BoolOp::And => " and ",
559                    ast::BoolOp::Or => " or ",
560                };
561                boolop
562                    .values
563                    .iter()
564                    .map(|v| self.expr_to_string_with_placeholders(v, param_names))
565                    .collect::<Vec<_>>()
566                    .join(op_str)
567            }
568            ast::Expr::UnaryOp(unaryop) => {
569                let operand = self.expr_to_string_with_placeholders(&unaryop.operand, param_names);
570                let op_str = match unaryop.op {
571                    ast::UnaryOp::Invert => "~",
572                    ast::UnaryOp::Not => "not ",
573                    ast::UnaryOp::UAdd => "+",
574                    ast::UnaryOp::USub => "-",
575                };
576                format!("{}{}", op_str, operand)
577            }
578            ast::Expr::Await(await_expr) => {
579                self.expr_to_string_with_placeholders(&await_expr.value, param_names)
580            }
581            ast::Expr::Subscript(subscript) => {
582                let value_str =
583                    self.expr_to_string_with_placeholders(&subscript.value, param_names);
584                let slice_str =
585                    self.expr_to_string_with_placeholders(&subscript.slice, param_names);
586                format!("{}[{}]", value_str, slice_str)
587            }
588            ast::Expr::Slice(slice) => {
589                let lower = slice
590                    .lower
591                    .as_ref()
592                    .map(|e| self.expr_to_string_with_placeholders(e, param_names))
593                    .unwrap_or_default();
594                let upper = slice
595                    .upper
596                    .as_ref()
597                    .map(|e| self.expr_to_string_with_placeholders(e, param_names))
598                    .unwrap_or_default();
599                let step = slice
600                    .step
601                    .as_ref()
602                    .map(|e| format!(":{}", self.expr_to_string_with_placeholders(e, param_names)))
603                    .unwrap_or_default();
604                format!("{}:{}{}", lower, upper, step)
605            }
606            ast::Expr::Constant(c) => match &c.value {
607                ast::Constant::Str(s) => {
608                    // Use proper string literal escaping
609                    format!(
610                        "\"{}\"",
611                        s.chars()
612                            .map(|c| match c {
613                                '"' => "\\\"".to_string(),
614                                '\\' => "\\\\".to_string(),
615                                '\n' => "\\n".to_string(),
616                                '\r' => "\\r".to_string(),
617                                '\t' => "\\t".to_string(),
618                                c if c.is_control() => format!("\\x{:02x}", c as u8),
619                                c => c.to_string(),
620                            })
621                            .collect::<String>()
622                    )
623                }
624                ast::Constant::Int(i) => i.to_string(),
625                ast::Constant::Float(f) => f.to_string(),
626                ast::Constant::Bool(b) => if *b { "True" } else { "False" }.to_string(),
627                ast::Constant::None => "None".to_string(),
628                ast::Constant::Bytes(b) => {
629                    // Properly escape bytes literal
630                    let escaped = b
631                        .iter()
632                        .map(|&byte| match byte {
633                            b'"' => "\\\"".to_string(),
634                            b'\\' => "\\\\".to_string(),
635                            b'\n' => "\\n".to_string(),
636                            b'\r' => "\\r".to_string(),
637                            b'\t' => "\\t".to_string(),
638                            b'\0' => "\\x00".to_string(),
639                            b if b.is_ascii_graphic() || b == b' ' => (b as char).to_string(),
640                            b => format!("\\x{:02x}", b),
641                        })
642                        .collect::<String>();
643                    format!("b\"{}\"", escaped)
644                }
645                ast::Constant::Complex { real, imag } => {
646                    if *real == 0.0 {
647                        format!("{}j", imag)
648                    } else if *imag >= 0.0 {
649                        format!("{} + {}j", real, imag)
650                    } else {
651                        format!("{} - {}j", real, -imag)
652                    }
653                }
654                ast::Constant::Ellipsis => "...".to_string(),
655                _ => "...".to_string(),
656            },
657            ast::Expr::ListComp(comp) => {
658                let elt = self.expr_to_string_with_placeholders(&comp.elt, param_names);
659
660                // Handle all generators (for multiple 'for' clauses)
661                let mut generators = Vec::new();
662                for gen in &comp.generators {
663                    let target = self.expr_to_string_with_placeholders(&gen.target, param_names);
664                    let iter = self.expr_to_string_with_placeholders(&gen.iter, param_names);
665                    let mut gen_str = format!("for {} in {}", target, iter);
666
667                    // Add conditions for this generator
668                    if !gen.ifs.is_empty() {
669                        let conds: Vec<String> = gen
670                            .ifs
671                            .iter()
672                            .map(|if_expr| {
673                                self.expr_to_string_with_placeholders(if_expr, param_names)
674                            })
675                            .collect();
676                        gen_str.push_str(&format!(" if {}", conds.join(" if ")));
677                    }
678
679                    generators.push(gen_str);
680                }
681
682                format!("[{} {}]", elt, generators.join(" "))
683            }
684            ast::Expr::SetComp(comp) => {
685                let elt = self.expr_to_string_with_placeholders(&comp.elt, param_names);
686
687                // Handle all generators (for multiple 'for' clauses)
688                let mut generators = Vec::new();
689                for gen in &comp.generators {
690                    let target = self.expr_to_string_with_placeholders(&gen.target, param_names);
691                    let iter = self.expr_to_string_with_placeholders(&gen.iter, param_names);
692                    let mut gen_str = format!("for {} in {}", target, iter);
693
694                    // Add conditions for this generator
695                    if !gen.ifs.is_empty() {
696                        let conds: Vec<String> = gen
697                            .ifs
698                            .iter()
699                            .map(|if_expr| {
700                                self.expr_to_string_with_placeholders(if_expr, param_names)
701                            })
702                            .collect();
703                        gen_str.push_str(&format!(" if {}", conds.join(" if ")));
704                    }
705
706                    generators.push(gen_str);
707                }
708
709                format!("{{{} {}}}", elt, generators.join(" "))
710            }
711            ast::Expr::DictComp(comp) => {
712                let key = self.expr_to_string_with_placeholders(&comp.key, param_names);
713                let value = self.expr_to_string_with_placeholders(&comp.value, param_names);
714
715                // Handle all generators (for multiple 'for' clauses)
716                let mut generators = Vec::new();
717                for gen in &comp.generators {
718                    let target = self.expr_to_string_with_placeholders(&gen.target, param_names);
719                    let iter = self.expr_to_string_with_placeholders(&gen.iter, param_names);
720                    let mut gen_str = format!("for {} in {}", target, iter);
721
722                    // Add conditions for this generator
723                    if !gen.ifs.is_empty() {
724                        let conds: Vec<String> = gen
725                            .ifs
726                            .iter()
727                            .map(|if_expr| {
728                                self.expr_to_string_with_placeholders(if_expr, param_names)
729                            })
730                            .collect();
731                        gen_str.push_str(&format!(" if {}", conds.join(" if ")));
732                    }
733
734                    generators.push(gen_str);
735                }
736
737                format!("{{{}: {} {}}}", key, value, generators.join(" "))
738            }
739            ast::Expr::GeneratorExp(gen) => {
740                let elt = self.expr_to_string_with_placeholders(&gen.elt, param_names);
741
742                // Handle all generators (for multiple 'for' clauses)
743                let mut generators = Vec::new();
744                for gen_item in &gen.generators {
745                    let target =
746                        self.expr_to_string_with_placeholders(&gen_item.target, param_names);
747                    let iter = self.expr_to_string_with_placeholders(&gen_item.iter, param_names);
748                    let mut gen_str = format!("for {} in {}", target, iter);
749
750                    // Add conditions for this generator
751                    if !gen_item.ifs.is_empty() {
752                        let conds: Vec<String> = gen_item
753                            .ifs
754                            .iter()
755                            .map(|if_expr| {
756                                self.expr_to_string_with_placeholders(if_expr, param_names)
757                            })
758                            .collect();
759                        gen_str.push_str(&format!(" if {}", conds.join(" if ")));
760                    }
761
762                    generators.push(gen_str);
763                }
764
765                format!("({} {})", elt, generators.join(" "))
766            }
767            ast::Expr::List(list) => {
768                let elements: Vec<String> = list
769                    .elts
770                    .iter()
771                    .map(|e| self.expr_to_string_with_placeholders(e, param_names))
772                    .collect();
773                format!("[{}]", elements.join(", "))
774            }
775            ast::Expr::Tuple(tuple) => {
776                let elements: Vec<String> = tuple
777                    .elts
778                    .iter()
779                    .map(|e| self.expr_to_string_with_placeholders(e, param_names))
780                    .collect();
781                if elements.len() == 1 {
782                    format!("({},)", elements[0])
783                } else {
784                    format!("({})", elements.join(", "))
785                }
786            }
787            ast::Expr::Dict(dict) => {
788                let mut items = Vec::new();
789                for (key, value) in dict.keys.iter().zip(&dict.values) {
790                    if let Some(k) = key {
791                        items.push(format!(
792                            "{}: {}",
793                            self.expr_to_string_with_placeholders(k, param_names),
794                            self.expr_to_string_with_placeholders(value, param_names)
795                        ));
796                    } else {
797                        items.push(format!(
798                            "**{}",
799                            self.expr_to_string_with_placeholders(value, param_names)
800                        ));
801                    }
802                }
803                format!("{{{}}}", items.join(", "))
804            }
805            _ => {
806                // For unsupported expression types, try to provide a reasonable fallback
807                match expr {
808                    ast::Expr::IfExp(ifexp) => {
809                        let body = self.expr_to_string_with_placeholders(&ifexp.body, param_names);
810                        let test = self.expr_to_string_with_placeholders(&ifexp.test, param_names);
811                        let orelse =
812                            self.expr_to_string_with_placeholders(&ifexp.orelse, param_names);
813                        format!("{} if {} else {}", body, test, orelse)
814                    }
815                    ast::Expr::Lambda(lambda) => {
816                        let args: Vec<String> = lambda
817                            .args
818                            .args
819                            .iter()
820                            .map(|arg| arg.def.arg.to_string())
821                            .collect();
822                        let body = self.expr_to_string_with_placeholders(&lambda.body, param_names);
823                        format!("lambda {}: {}", args.join(", "), body)
824                    }
825                    ast::Expr::JoinedStr(joined) => {
826                        let mut result = String::from("f\"");
827                        for value in &joined.values {
828                            match value {
829                                ast::Expr::Constant(c) => {
830                                    if let ast::Constant::Str(s) = &c.value {
831                                        result.push_str(s);
832                                    }
833                                }
834                                ast::Expr::FormattedValue(fmt) => {
835                                    result.push('{');
836                                    let var_str = self
837                                        .expr_to_string_with_placeholders(&fmt.value, param_names);
838                                    result.push_str(&var_str);
839                                    match fmt.conversion {
840                                        ast::ConversionFlag::Str => result.push_str("!s"),
841                                        ast::ConversionFlag::Repr => result.push_str("!r"),
842                                        ast::ConversionFlag::Ascii => result.push_str("!a"),
843                                        ast::ConversionFlag::None => {}
844                                    }
845                                    if let Some(spec) = &fmt.format_spec {
846                                        result.push(':');
847                                        match &**spec {
848                                            ast::Expr::Constant(c) => {
849                                                if let ast::Constant::Str(s) = &c.value {
850                                                    result.push_str(s);
851                                                }
852                                            }
853                                            ast::Expr::JoinedStr(_) => {
854                                                // For f-strings inside format specs, we need special handling
855                                                let reconstructed = self
856                                                    .expr_to_string_with_placeholders(
857                                                        spec,
858                                                        param_names,
859                                                    );
860                                                // Remove the f" prefix and " suffix if it's an f-string
861                                                if reconstructed.starts_with("f\"")
862                                                    && reconstructed.ends_with('"')
863                                                {
864                                                    result.push_str(
865                                                        &reconstructed[2..reconstructed.len() - 1],
866                                                    );
867                                                } else {
868                                                    result.push_str(&reconstructed);
869                                                }
870                                            }
871                                            _ => result.push_str(
872                                                &self.expr_to_string_with_placeholders(
873                                                    spec,
874                                                    param_names,
875                                                ),
876                                            ),
877                                        }
878                                    }
879                                    result.push('}');
880                                }
881                                _ => {
882                                    result.push('{');
883                                    let other_str =
884                                        self.expr_to_string_with_placeholders(value, param_names);
885                                    result.push_str(&other_str);
886                                    result.push('}');
887                                }
888                            }
889                        }
890                        result.push('"');
891                        result
892                    }
893                    ast::Expr::FormattedValue(fmt) => {
894                        // This should normally be handled within JoinedStr
895                        let mut result = String::new();
896                        result.push('{');
897                        result.push_str(
898                            &self.expr_to_string_with_placeholders(&fmt.value, param_names),
899                        );
900                        match fmt.conversion {
901                            ast::ConversionFlag::Str => result.push_str("!s"),
902                            ast::ConversionFlag::Repr => result.push_str("!r"),
903                            ast::ConversionFlag::Ascii => result.push_str("!a"),
904                            ast::ConversionFlag::None => {}
905                        }
906                        if let Some(spec) = &fmt.format_spec {
907                            result.push(':');
908                            match &**spec {
909                                ast::Expr::Constant(c) => {
910                                    if let ast::Constant::Str(s) = &c.value {
911                                        result.push_str(s);
912                                    }
913                                }
914                                _ => result.push_str(
915                                    &self.expr_to_string_with_placeholders(spec, param_names),
916                                ),
917                            }
918                        }
919                        result.push('}');
920                        result
921                    }
922                    ast::Expr::NamedExpr(named) => {
923                        format!(
924                            "{} := {}",
925                            self.expr_to_string_with_placeholders(&named.target, param_names),
926                            self.expr_to_string_with_placeholders(&named.value, param_names)
927                        )
928                    }
929                    ast::Expr::Set(set) => {
930                        let elements: Vec<String> = set
931                            .elts
932                            .iter()
933                            .map(|e| self.expr_to_string_with_placeholders(e, param_names))
934                            .collect();
935                        if elements.is_empty() {
936                            "set()".to_string()
937                        } else {
938                            format!("{{{}}}", elements.join(", "))
939                        }
940                    }
941                    ast::Expr::Yield(yield_expr) => {
942                        if let Some(value) = &yield_expr.value {
943                            format!(
944                                "yield {}",
945                                self.expr_to_string_with_placeholders(value, param_names)
946                            )
947                        } else {
948                            "yield".to_string()
949                        }
950                    }
951                    ast::Expr::YieldFrom(yieldfrom) => {
952                        format!(
953                            "yield from {}",
954                            self.expr_to_string_with_placeholders(&yieldfrom.value, param_names)
955                        )
956                    }
957                    _ => "/* unsupported expr */".to_string(),
958                }
959            }
960        }
961    }
962
963    /// Extract replacement expression from async function body
964    fn extract_replacement_from_async_function(
965        &self,
966        func: &ast::StmtAsyncFunctionDef,
967    ) -> Result<(String, ast::Expr), ReplacementExtractionError> {
968        // Skip docstring and pass statements
969        let body_stmts: Vec<&ast::Stmt> = func
970            .body
971            .iter()
972            .skip_while(|stmt| {
973                matches!(stmt, ast::Stmt::Expr(expr_stmt) if matches!(expr_stmt.value.as_ref(),
974                    ast::Expr::Constant(c) if matches!(&c.value, ast::Constant::Str(_))))
975            })
976            .filter(|stmt| !matches!(stmt, ast::Stmt::Pass(_)))
977            .collect();
978
979        if body_stmts.is_empty() {
980            // Empty body is valid - means remove the function completely
981            return Ok((
982                "".to_string(),
983                ast::Expr::Constant(ast::ExprConstant {
984                    value: ast::Constant::Str("".to_string()),
985                    kind: None,
986                    range: Default::default(),
987                }),
988            ));
989        }
990
991        if body_stmts.len() > 1 {
992            return Err(ReplacementExtractionError::new(
993                func.name.to_string(),
994                ReplacementFailureReason::MultipleStatements,
995                "Function body contains multiple statements".to_string(),
996            ));
997        }
998
999        // Extract return expression
1000        match body_stmts[0] {
1001            ast::Stmt::Return(ret_stmt) => {
1002                if let Some(value) = &ret_stmt.value {
1003                    // Get function parameters for placeholder conversion
1004                    let param_names: HashSet<String> = func
1005                        .args
1006                        .args
1007                        .iter()
1008                        .map(|arg| arg.def.arg.to_string())
1009                        .collect();
1010
1011                    let replacement_expr =
1012                        self.expr_to_string_with_placeholders(value, &param_names);
1013                    Ok((replacement_expr, value.as_ref().clone()))
1014                } else {
1015                    Err(ReplacementExtractionError::new(
1016                        func.name.to_string(),
1017                        ReplacementFailureReason::NoReturnStatement,
1018                        "Return statement has no value".to_string(),
1019                    ))
1020                }
1021            }
1022            _ => Err(ReplacementExtractionError::new(
1023                func.name.to_string(),
1024                ReplacementFailureReason::NoReturnStatement,
1025                "Function body must contain only a return statement".to_string(),
1026            )),
1027        }
1028    }
1029
1030    /// Visit a function definition
1031    fn visit_function(&mut self, func: &ast::StmtFunctionDef) {
1032        // Track this as a locally defined function (even if not decorated)
1033        if self.class_stack.is_empty() {
1034            // Only track top-level functions, not methods
1035            self.local_functions.insert(func.name.to_string());
1036        }
1037
1038        if !Self::has_replace_me_decorator(&func.decorator_list) {
1039            return;
1040        }
1041
1042        let full_path = self.build_full_path(&func.name);
1043        let parameters = self.extract_parameters(func);
1044        let since = self.extract_since_version(&func.decorator_list);
1045        let remove_in = self.extract_remove_in_version(&func.decorator_list);
1046        let message = Self::extract_message(&func.decorator_list);
1047
1048        // Determine construct type
1049        let construct_type = if self.class_stack.is_empty() {
1050            ConstructType::Function
1051        } else {
1052            let decorator_names: Vec<&str> = func
1053                .decorator_list
1054                .iter()
1055                .filter_map(|dec| {
1056                    if let ast::Expr::Name(name) = dec {
1057                        Some(name.id.as_str())
1058                    } else {
1059                        None
1060                    }
1061                })
1062                .collect();
1063
1064            if decorator_names.contains(&"property") {
1065                ConstructType::Property
1066            } else if decorator_names.contains(&"classmethod") {
1067                ConstructType::ClassMethod
1068            } else if decorator_names.contains(&"staticmethod") {
1069                ConstructType::StaticMethod
1070            } else {
1071                ConstructType::Function
1072            }
1073        };
1074
1075        // Try to extract replacement from decorator first, then from function body
1076        let replacement_result = if let Some((replacement_expr, ast)) =
1077            self.extract_replacement_from_decorator(&func.decorator_list)
1078        {
1079            Ok((replacement_expr, ast))
1080        } else {
1081            self.extract_replacement_from_function(func)
1082        };
1083
1084        match replacement_result {
1085            Ok((replacement_expr, ast)) => {
1086                let mut replace_info =
1087                    ReplaceInfo::new(&full_path, &replacement_expr, construct_type);
1088                // Store the AST
1089                replace_info.replacement_ast = Some(Box::new(ast));
1090                replace_info.parameters = parameters;
1091                replace_info.since = since.and_then(|s| s.parse().ok());
1092                replace_info.remove_in = remove_in.and_then(|s| s.parse().ok());
1093                replace_info.message = message;
1094                self.replacements.insert(full_path, replace_info);
1095            }
1096            Err(e) => {
1097                let unreplaceable = UnreplaceableNode::new(
1098                    full_path.clone(),
1099                    e.reason(),
1100                    e.to_string(),
1101                    construct_type,
1102                );
1103                self.unreplaceable.insert(full_path, unreplaceable);
1104            }
1105        }
1106    }
1107
1108    /// Visit an async function definition
1109    fn visit_async_function(&mut self, func: &ast::StmtAsyncFunctionDef) {
1110        if !Self::has_replace_me_decorator(&func.decorator_list) {
1111            return;
1112        }
1113
1114        let full_path = self.build_full_path(&func.name);
1115        let parameters = self.extract_async_parameters(func);
1116        let since = self.extract_since_version(&func.decorator_list);
1117        let remove_in = self.extract_remove_in_version(&func.decorator_list);
1118        let message = Self::extract_message(&func.decorator_list);
1119
1120        // Determine construct type
1121        let construct_type = if self.class_stack.is_empty() {
1122            ConstructType::Function
1123        } else {
1124            let decorator_names: Vec<&str> = func
1125                .decorator_list
1126                .iter()
1127                .filter_map(|dec| {
1128                    if let ast::Expr::Name(name) = dec {
1129                        Some(name.id.as_str())
1130                    } else {
1131                        None
1132                    }
1133                })
1134                .collect();
1135
1136            if decorator_names.contains(&"property") {
1137                ConstructType::Property
1138            } else if decorator_names.contains(&"classmethod") {
1139                ConstructType::ClassMethod
1140            } else if decorator_names.contains(&"staticmethod") {
1141                ConstructType::StaticMethod
1142            } else {
1143                ConstructType::Function
1144            }
1145        };
1146
1147        // Try to extract replacement from decorator first, then from function body
1148        let replacement_result = if let Some((replacement_expr, ast)) =
1149            self.extract_replacement_from_decorator(&func.decorator_list)
1150        {
1151            Ok((replacement_expr, ast))
1152        } else {
1153            self.extract_replacement_from_async_function(func)
1154        };
1155
1156        match replacement_result {
1157            Ok((replacement_expr, ast)) => {
1158                let mut replace_info =
1159                    ReplaceInfo::new(&full_path, &replacement_expr, construct_type);
1160                // Store the AST
1161                replace_info.replacement_ast = Some(Box::new(ast));
1162                replace_info.parameters = parameters;
1163                replace_info.since = since.and_then(|s| s.parse().ok());
1164                replace_info.remove_in = remove_in.and_then(|s| s.parse().ok());
1165                replace_info.message = message;
1166                self.replacements.insert(full_path, replace_info);
1167            }
1168            Err(e) => {
1169                let unreplaceable = UnreplaceableNode::new(
1170                    full_path.clone(),
1171                    e.reason(),
1172                    e.to_string(),
1173                    construct_type,
1174                );
1175                self.unreplaceable.insert(full_path, unreplaceable);
1176            }
1177        }
1178    }
1179
1180    /// Visit a class definition
1181    fn visit_class(&mut self, class_def: &ast::StmtClassDef) {
1182        let class_name = class_def.name.to_string();
1183        let full_class_name = self.build_full_path(&class_name);
1184
1185        // Track this as a locally defined class
1186        self.local_classes.insert(class_name.clone());
1187
1188        // Record base classes
1189        let mut bases = Vec::new();
1190        for base in &class_def.bases {
1191            match base {
1192                ast::Expr::Name(name) => {
1193                    let base_name = name.id.to_string();
1194                    let qualified_name = format!("{}.{}", self.module_name, base_name);
1195                    bases.push(qualified_name);
1196                }
1197                ast::Expr::Attribute(_) => {
1198                    let qualified_name = self.build_qualified_name_from_expr(base);
1199                    bases.push(qualified_name);
1200                }
1201                _ => {}
1202            }
1203        }
1204
1205        if !bases.is_empty() {
1206            tracing::debug!("Class {} inherits from: {:?}", full_class_name, bases);
1207            self.inheritance_map.insert(full_class_name.clone(), bases);
1208        }
1209
1210        // Check if class itself has @replace_me
1211        if Self::has_replace_me_decorator(&class_def.decorator_list) {
1212            if let Some(init_replacement) = self.extract_class_replacement(class_def) {
1213                let mut replace_info =
1214                    ReplaceInfo::new(&full_class_name, &init_replacement, ConstructType::Class);
1215
1216                // Extract __init__ parameters
1217                for stmt in &class_def.body {
1218                    if let ast::Stmt::FunctionDef(func) = stmt {
1219                        if func.name.as_str() == "__init__" {
1220                            replace_info.parameters = self
1221                                .extract_parameters(func)
1222                                .into_iter()
1223                                .filter(|p| p.name != "self")
1224                                .collect();
1225                            break;
1226                        }
1227                    }
1228                }
1229
1230                self.replacements
1231                    .insert(full_class_name.clone(), replace_info);
1232            } else {
1233                let unreplaceable = UnreplaceableNode::new(
1234                    full_class_name.clone(),
1235                    ReplacementFailureReason::NoInitMethod,
1236                    "Class has @replace_me decorator but no __init__ method with clear replacement pattern".to_string(),
1237                    ConstructType::Class,
1238                );
1239                self.unreplaceable
1240                    .insert(full_class_name.clone(), unreplaceable);
1241            }
1242        }
1243
1244        // Visit class body
1245        self.class_stack.push(class_name);
1246        for stmt in &class_def.body {
1247            self.visit_stmt(stmt);
1248        }
1249        self.class_stack.pop();
1250    }
1251
1252    /// Extract replacement from class __init__ method
1253    fn extract_class_replacement(&self, class_def: &ast::StmtClassDef) -> Option<String> {
1254        for stmt in &class_def.body {
1255            if let ast::Stmt::FunctionDef(func) = stmt {
1256                if func.name.as_str() == "__init__" {
1257                    // Get parameter names from __init__ (excluding self)
1258                    let param_names: HashSet<String> = self
1259                        .extract_parameters(func)
1260                        .into_iter()
1261                        .filter(|p| p.name != "self" && !p.is_vararg && !p.is_kwarg)
1262                        .map(|p| p.name)
1263                        .collect();
1264
1265                    for init_stmt in &func.body {
1266                        if let ast::Stmt::Assign(assign) = init_stmt {
1267                            if assign.targets.len() == 1 {
1268                                if let ast::Expr::Attribute(attr) = &assign.targets[0] {
1269                                    if let ast::Expr::Name(name) = &*attr.value {
1270                                        if name.id.as_str() == "self" {
1271                                            // Found self.attr = expr, use placeholders for parameters
1272                                            let replacement_expr = self
1273                                                .expr_to_string_with_placeholders(
1274                                                    &assign.value,
1275                                                    &param_names,
1276                                                );
1277                                            return Some(replacement_expr);
1278                                        }
1279                                    }
1280                                }
1281                            }
1282                        }
1283                    }
1284                }
1285            }
1286        }
1287        None
1288    }
1289
1290    fn visit_assign(&mut self, assign: &ast::StmtAssign) {
1291        // Handle module-level assignments like OLD_CONSTANT = replace_me(42)
1292        if assign.targets.len() == 1 {
1293            if let ast::Expr::Name(name) = &assign.targets[0] {
1294                // Check if the assignment value is a replace_me call
1295                if let ast::Expr::Call(call) = assign.value.as_ref() {
1296                    if matches!(&*call.func, ast::Expr::Name(func_name) if func_name.id.as_str() == "replace_me")
1297                    {
1298                        // Extract the replacement value
1299                        if let Some(arg) = call.args.first() {
1300                            let replacement_expr = self.expr_to_string(arg);
1301
1302                            let full_name = if self.class_stack.is_empty() {
1303                                format!("{}.{}", self.module_name, name.id)
1304                            } else {
1305                                format!(
1306                                    "{}.{}.{}",
1307                                    self.module_name,
1308                                    self.class_stack.join("."),
1309                                    name.id
1310                                )
1311                            };
1312
1313                            let construct_type = if self.class_stack.is_empty() {
1314                                ConstructType::ModuleAttribute
1315                            } else {
1316                                ConstructType::ClassAttribute
1317                            };
1318
1319                            let replace_info = ReplaceInfo {
1320                                old_name: full_name.clone(),
1321                                replacement_expr,
1322                                replacement_ast: None, // rustpython AST types differ
1323                                construct_type,
1324                                parameters: vec![],
1325                                return_type: None,
1326                                since: None,
1327                                remove_in: None,
1328                                message: None,
1329                            };
1330
1331                            self.replacements.insert(full_name, replace_info);
1332                        }
1333                    }
1334                }
1335            }
1336        }
1337    }
1338
1339    fn visit_stmt(&mut self, stmt: &ast::Stmt) {
1340        match stmt {
1341            ast::Stmt::FunctionDef(func) => self.visit_function(func),
1342            ast::Stmt::AsyncFunctionDef(func) => self.visit_async_function(func),
1343            ast::Stmt::ClassDef(class) => self.visit_class(class),
1344            ast::Stmt::Import(import) => {
1345                for alias in &import.names {
1346                    self.imports.push(ImportInfo::new(
1347                        alias.name.to_string(),
1348                        vec![(
1349                            alias.name.to_string(),
1350                            alias.asname.as_ref().map(|n| n.to_string()),
1351                        )],
1352                    ));
1353                }
1354            }
1355            ast::Stmt::ImportFrom(import) => {
1356                let names: Vec<(String, Option<String>)> = import
1357                    .names
1358                    .iter()
1359                    .map(|alias| {
1360                        (
1361                            alias.name.to_string(),
1362                            alias.asname.as_ref().map(|n| n.to_string()),
1363                        )
1364                    })
1365                    .collect();
1366
1367                let module_name = if let Some(module) = &import.module {
1368                    let level = import.level.as_ref().map_or(0, |i| i.to_usize());
1369                    let dots = ".".repeat(level);
1370                    format!("{}{}", dots, module)
1371                } else {
1372                    let level = import.level.as_ref().map_or(0, |i| i.to_usize());
1373                    ".".repeat(level)
1374                };
1375
1376                self.imports.push(ImportInfo::new(module_name, names));
1377            }
1378            ast::Stmt::Assign(assign) => {
1379                self.visit_assign(assign);
1380            }
1381            ast::Stmt::AnnAssign(ann_assign) => {
1382                // Handle annotated assignments like DEFAULT_TIMEOUT: int = replace_me(30)
1383                if let Some(value) = &ann_assign.value {
1384                    if let ast::Expr::Name(name) = ann_assign.target.as_ref() {
1385                        if let ast::Expr::Call(call) = value.as_ref() {
1386                            if matches!(&*call.func, ast::Expr::Name(func_name) if func_name.id.as_str() == "replace_me")
1387                            {
1388                                if let Some(arg) = call.args.first() {
1389                                    let replacement_expr = self.expr_to_string(arg);
1390
1391                                    let full_name = if self.class_stack.is_empty() {
1392                                        format!("{}.{}", self.module_name, name.id)
1393                                    } else {
1394                                        format!(
1395                                            "{}.{}.{}",
1396                                            self.module_name,
1397                                            self.class_stack.join("."),
1398                                            name.id
1399                                        )
1400                                    };
1401
1402                                    let construct_type = if self.class_stack.is_empty() {
1403                                        ConstructType::ModuleAttribute
1404                                    } else {
1405                                        ConstructType::ClassAttribute
1406                                    };
1407
1408                                    let replace_info = ReplaceInfo {
1409                                        old_name: full_name.clone(),
1410                                        replacement_expr,
1411                                        replacement_ast: None, // rustpython AST types differ
1412                                        construct_type,
1413                                        parameters: vec![],
1414                                        return_type: None,
1415                                        since: None,
1416                                        remove_in: None,
1417                                        message: None,
1418                                    };
1419
1420                                    self.replacements.insert(full_name, replace_info);
1421                                }
1422                            }
1423                        }
1424                    }
1425                }
1426            }
1427            ast::Stmt::If(if_stmt) => {
1428                // Visit the if body
1429                for stmt in &if_stmt.body {
1430                    self.visit_stmt(stmt);
1431                }
1432                // Visit the else body
1433                for stmt in &if_stmt.orelse {
1434                    self.visit_stmt(stmt);
1435                }
1436            }
1437            ast::Stmt::Try(try_stmt) => {
1438                // Visit try body
1439                for stmt in &try_stmt.body {
1440                    self.visit_stmt(stmt);
1441                }
1442                // Visit except handlers
1443                for handler in &try_stmt.handlers {
1444                    let ast::ExceptHandler::ExceptHandler(h) = handler;
1445                    for stmt in &h.body {
1446                        self.visit_stmt(stmt);
1447                    }
1448                }
1449                // Visit else body
1450                for stmt in &try_stmt.orelse {
1451                    self.visit_stmt(stmt);
1452                }
1453                // Visit finally body
1454                for stmt in &try_stmt.finalbody {
1455                    self.visit_stmt(stmt);
1456                }
1457            }
1458            ast::Stmt::While(while_stmt) => {
1459                // Visit while body
1460                for stmt in &while_stmt.body {
1461                    self.visit_stmt(stmt);
1462                }
1463                // Visit else body
1464                for stmt in &while_stmt.orelse {
1465                    self.visit_stmt(stmt);
1466                }
1467            }
1468            ast::Stmt::For(for_stmt) => {
1469                // Visit for body
1470                for stmt in &for_stmt.body {
1471                    self.visit_stmt(stmt);
1472                }
1473                // Visit else body
1474                for stmt in &for_stmt.orelse {
1475                    self.visit_stmt(stmt);
1476                }
1477            }
1478            ast::Stmt::With(with_stmt) => {
1479                // Visit with body
1480                for stmt in &with_stmt.body {
1481                    self.visit_stmt(stmt);
1482                }
1483            }
1484            _ => {}
1485        }
1486    }
1487}
1488
1489impl ReplacementExtractionError {
1490    fn reason(&self) -> ReplacementFailureReason {
1491        match self {
1492            Self::ExtractionFailed { reason, .. } => reason.clone(),
1493        }
1494    }
1495}