Skip to main content

dissolve_python/
rustpython_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//! RustPython-based AST visitor for function call replacement
16
17use anyhow::Result;
18use rustpython_ast::{self as ast, Ranged};
19use std::cell::RefCell;
20use std::collections::{HashMap, HashSet};
21use std::rc::Rc;
22use thiserror::Error;
23use tracing;
24
25#[derive(Debug, Error)]
26pub enum TypeIntrospectionError {
27    #[error("Pyright query failed: {0}")]
28    PyrightError(String),
29
30    #[error("Mypy query failed: {0}")]
31    MypyError(String),
32
33    #[error("Failed to determine position in source: {0}")]
34    PositionError(String),
35
36    #[error("No type introspection client available")]
37    NoClientAvailable,
38}
39
40use crate::ast_utils;
41use crate::core::{ParameterInfo, ReplaceInfo};
42use crate::type_introspection_context::TypeIntrospectionContext;
43use crate::types::TypeIntrospectionMethod;
44
45/// Visitor to find and replace function calls using rustpython AST  
46pub struct RustPythonFunctionCallReplacer<'a> {
47    replacements_info: HashMap<String, ReplaceInfo>,
48    replacements: Vec<(std::ops::Range<usize>, String)>, // (source_range, replacement_string)
49    type_introspection: TypeIntrospectionMethod,
50    file_path: String,
51    module_name: String,
52    import_map: HashMap<String, String>,
53    source_content: String,
54    inheritance_map: HashMap<String, Vec<String>>,
55    pyright_client: Option<Rc<RefCell<Box<dyn crate::pyright_lsp::PyrightLspClientTrait>>>>,
56    mypy_client: Option<Rc<RefCell<crate::mypy_lsp::MypyTypeIntrospector>>>,
57    type_cache: RefCell<HashMap<(u32, u32), Option<String>>>,
58    _type_context: &'a mut TypeIntrospectionContext,
59    builtins: HashSet<String>,
60    module_functions: HashSet<String>, // Functions defined in this module
61}
62
63#[allow(dead_code)] // Some methods are kept for potential future use
64impl<'a> RustPythonFunctionCallReplacer<'a> {
65    pub fn new_with_context(
66        replacements_info: HashMap<String, ReplaceInfo>,
67        type_introspection_context: &'a mut TypeIntrospectionContext,
68        file_path: String,
69        module_name: String,
70        source_content: String,
71        inheritance_map: HashMap<String, Vec<String>>,
72        builtins: HashSet<String>,
73    ) -> Result<Self> {
74        let type_introspection = type_introspection_context.method();
75
76        // Get the clients from the context
77        let pyright_client = type_introspection_context.pyright_client();
78        let mypy_client = type_introspection_context.mypy_client();
79
80        Ok(Self {
81            replacements_info,
82            replacements: Vec::new(),
83            type_introspection,
84            file_path,
85            module_name,
86            import_map: HashMap::new(),
87            source_content,
88            inheritance_map,
89            pyright_client,
90            mypy_client,
91            type_cache: RefCell::new(HashMap::new()),
92            _type_context: type_introspection_context,
93            builtins,
94            module_functions: HashSet::new(),
95        })
96    }
97
98    pub fn get_replacements(mut self) -> Vec<(std::ops::Range<usize>, String)> {
99        std::mem::take(&mut self.replacements)
100    }
101
102    /// Visit a module
103    pub fn visit_module(&mut self, module: &[ast::Stmt]) {
104        tracing::debug!("Visiting module with {} statements", module.len());
105
106        // First pass: build import map
107        for stmt in module {
108            if let ast::Stmt::ImportFrom(import) = stmt {
109                let level = import.level.as_ref().map_or(0, |i| i.to_usize());
110                if let Some(module) = &import.module {
111                    // Resolve relative imports - check the level field for relative imports
112                    let resolved_module = if level > 0 {
113                        // Relative import - resolve to full module path
114                        let module_parts: Vec<&str> = self.module_name.split('.').collect();
115                        // Go up 'level' directories from current module
116                        let parent_parts = if module_parts.len() > level {
117                            &module_parts[..module_parts.len() - level]
118                        } else {
119                            // Can't go up that many levels, use root
120                            &module_parts[..1]
121                        };
122
123                        let parent = parent_parts.join(".");
124                        if module.is_empty() {
125                            // from . import X (module is the parent itself)
126                            parent
127                        } else {
128                            // from .submodule import X
129                            format!("{}.{}", parent, module)
130                        }
131                    } else {
132                        module.to_string()
133                    };
134
135                    for alias in &import.names {
136                        let full_name = format!("{}.{}", resolved_module, alias.name);
137                        // Only track imports without aliases
138                        if alias.asname.is_none() {
139                            self.import_map.insert(alias.name.to_string(), full_name);
140                        }
141                    }
142                }
143            }
144        }
145
146        // Second pass: collect all function definitions in the module
147        for stmt in module {
148            match stmt {
149                ast::Stmt::FunctionDef(func) => {
150                    self.module_functions.insert(func.name.to_string());
151                }
152                ast::Stmt::AsyncFunctionDef(func) => {
153                    self.module_functions.insert(func.name.to_string());
154                }
155                ast::Stmt::ClassDef(class) => {
156                    // Also track classes as they might contain methods
157                    for body_stmt in &class.body {
158                        if let ast::Stmt::FunctionDef(func) = body_stmt {
159                            // Track class methods with their qualified name
160                            self.module_functions
161                                .insert(format!("{}.{}", class.name, func.name));
162                        }
163                    }
164                }
165                _ => {}
166            }
167        }
168
169        tracing::debug!(
170            "Found {} module functions: {:?}",
171            self.module_functions.len(),
172            self.module_functions
173        );
174
175        // Third pass: visit statements for replacements
176        for (i, stmt) in module.iter().enumerate() {
177            tracing::debug!("Visiting statement {}", i);
178            self.visit_stmt(stmt);
179        }
180    }
181
182    /// Visit a statement
183    fn visit_stmt(&mut self, stmt: &ast::Stmt) {
184        match stmt {
185            ast::Stmt::FunctionDef(func) => {
186                // Skip functions decorated with @replace_me
187                let has_replace_me = func.decorator_list.iter().any(|dec| match dec {
188                    ast::Expr::Name(name) => name.id.as_str() == "replace_me",
189                    ast::Expr::Call(call) => {
190                        if let ast::Expr::Name(name) = &*call.func {
191                            name.id.as_str() == "replace_me"
192                        } else {
193                            false
194                        }
195                    }
196                    _ => false,
197                });
198
199                if !has_replace_me {
200                    for s in &func.body {
201                        self.visit_stmt(s);
202                    }
203                }
204            }
205            ast::Stmt::AsyncFunctionDef(func) => {
206                tracing::debug!("Visiting async function: {}", func.name);
207                for s in &func.body {
208                    self.visit_stmt(s);
209                }
210            }
211            ast::Stmt::ClassDef(class) => {
212                for s in &class.body {
213                    self.visit_stmt(s);
214                }
215            }
216            ast::Stmt::For(for_stmt) => {
217                // Visit the target and iter expressions
218                self.visit_expr(&for_stmt.target);
219                self.visit_expr(&for_stmt.iter);
220                for s in &for_stmt.body {
221                    self.visit_stmt(s);
222                }
223                for s in &for_stmt.orelse {
224                    self.visit_stmt(s);
225                }
226            }
227            ast::Stmt::While(while_stmt) => {
228                // Visit the test expression
229                self.visit_expr(&while_stmt.test);
230                for s in &while_stmt.body {
231                    self.visit_stmt(s);
232                }
233                for s in &while_stmt.orelse {
234                    self.visit_stmt(s);
235                }
236            }
237            ast::Stmt::If(if_stmt) => {
238                // Visit the test expression (this is where walrus operators can be)
239                self.visit_expr(&if_stmt.test);
240                for s in &if_stmt.body {
241                    self.visit_stmt(s);
242                }
243                for s in &if_stmt.orelse {
244                    self.visit_stmt(s);
245                }
246            }
247            ast::Stmt::With(with) => {
248                // Visit the context expressions
249                for item in &with.items {
250                    self.visit_expr(&item.context_expr);
251                    if let Some(optional_vars) = &item.optional_vars {
252                        self.visit_expr(optional_vars);
253                    }
254                }
255                for s in &with.body {
256                    self.visit_stmt(s);
257                }
258            }
259            ast::Stmt::AsyncWith(with) => {
260                // Visit the context expressions
261                for item in &with.items {
262                    self.visit_expr(&item.context_expr);
263                    if let Some(optional_vars) = &item.optional_vars {
264                        self.visit_expr(optional_vars);
265                    }
266                }
267                for s in &with.body {
268                    self.visit_stmt(s);
269                }
270            }
271            ast::Stmt::Try(try_stmt) => {
272                for s in &try_stmt.body {
273                    self.visit_stmt(s);
274                }
275                for handler in &try_stmt.handlers {
276                    let ast::ExceptHandler::ExceptHandler(h) = handler;
277                    for s in &h.body {
278                        self.visit_stmt(s);
279                    }
280                }
281                for s in &try_stmt.orelse {
282                    self.visit_stmt(s);
283                }
284                for s in &try_stmt.finalbody {
285                    self.visit_stmt(s);
286                }
287            }
288            ast::Stmt::Expr(expr_stmt) => {
289                self.visit_expr(&expr_stmt.value);
290            }
291            ast::Stmt::Return(ret) => {
292                if let Some(value) = &ret.value {
293                    self.visit_expr(value);
294                }
295            }
296            ast::Stmt::Delete(del) => {
297                for target in &del.targets {
298                    self.visit_expr(target);
299                }
300            }
301            ast::Stmt::Assign(assign) => {
302                self.visit_expr(&assign.value);
303            }
304            ast::Stmt::AugAssign(aug) => {
305                self.visit_expr(&aug.value);
306            }
307            ast::Stmt::AnnAssign(ann) => {
308                if let Some(value) = &ann.value {
309                    self.visit_expr(value);
310                }
311            }
312            ast::Stmt::Import(import) => {
313                for alias in &import.names {
314                    if let Some(asname) = &alias.asname {
315                        self.import_map
316                            .insert(asname.to_string(), alias.name.to_string());
317                    } else {
318                        let parts: Vec<&str> = alias.name.split('.').collect();
319                        if let Some(first) = parts.first() {
320                            self.import_map
321                                .insert(first.to_string(), alias.name.to_string());
322                        }
323                    }
324                }
325            }
326            ast::Stmt::ImportFrom(_) => {
327                // Import mapping is handled in the first pass of visit_module
328            }
329            _ => {}
330        }
331    }
332
333    /// Visit an expression
334    fn visit_expr(&mut self, expr: &ast::Expr) {
335        match expr {
336            ast::Expr::Call(call) => {
337                let func_name = self.expr_to_string(&call.func);
338                tracing::info!("Visiting call expression: {}", func_name);
339
340                // Check if this is a magic method call
341                if let Some(replacement_str) = self.check_magic_method_call_direct(expr, call) {
342                    tracing::debug!("Found magic method replacement");
343                    let range = expr.range();
344                    let source_range = range.start().to_usize()..range.end().to_usize();
345                    self.replacements.push((source_range, replacement_str));
346                    return;
347                }
348
349                // Check if this is a regular function call that needs replacement
350                if let Some(replacement_str) =
351                    self.check_function_call_direct(&call.func, &call.args, &call.keywords)
352                {
353                    tracing::debug!(
354                        "Found function replacement for {}: {}",
355                        func_name,
356                        replacement_str
357                    );
358                    let range = expr.range();
359                    let source_range = range.start().to_usize()..range.end().to_usize();
360                    tracing::debug!("Adding replacement at range {:?}", source_range);
361                    self.replacements.push((source_range, replacement_str));
362                    return;
363                }
364
365                // Visit nested expressions
366                self.visit_expr(&call.func);
367                for arg in &call.args {
368                    self.visit_expr(arg);
369                }
370                for keyword in &call.keywords {
371                    self.visit_expr(&keyword.value);
372                }
373            }
374            ast::Expr::BinOp(binop) => {
375                tracing::debug!(
376                    "Visiting binary operation: {} op {}",
377                    self.expr_to_string(&binop.left),
378                    self.expr_to_string(&binop.right)
379                );
380                self.visit_expr(&binop.left);
381                self.visit_expr(&binop.right);
382            }
383            ast::Expr::UnaryOp(unary) => {
384                self.visit_expr(&unary.operand);
385            }
386            ast::Expr::IfExp(ifexp) => {
387                self.visit_expr(&ifexp.test);
388                self.visit_expr(&ifexp.body);
389                self.visit_expr(&ifexp.orelse);
390            }
391            ast::Expr::Dict(dict) => {
392                for key in dict.keys.iter().flatten() {
393                    self.visit_expr(key);
394                }
395                for value in &dict.values {
396                    self.visit_expr(value);
397                }
398            }
399            ast::Expr::Set(set) => {
400                for elt in &set.elts {
401                    self.visit_expr(elt);
402                }
403            }
404            ast::Expr::List(list) => {
405                for elt in &list.elts {
406                    self.visit_expr(elt);
407                }
408            }
409            ast::Expr::Tuple(tuple) => {
410                for elt in &tuple.elts {
411                    self.visit_expr(elt);
412                }
413            }
414            ast::Expr::ListComp(comp) => {
415                self.visit_expr(&comp.elt);
416                for gen in &comp.generators {
417                    self.visit_expr(&gen.iter);
418                    for if_expr in &gen.ifs {
419                        self.visit_expr(if_expr);
420                    }
421                }
422            }
423            ast::Expr::SetComp(comp) => {
424                self.visit_expr(&comp.elt);
425                for gen in &comp.generators {
426                    self.visit_expr(&gen.iter);
427                    for if_expr in &gen.ifs {
428                        self.visit_expr(if_expr);
429                    }
430                }
431            }
432            ast::Expr::DictComp(comp) => {
433                self.visit_expr(&comp.key);
434                self.visit_expr(&comp.value);
435                for gen in &comp.generators {
436                    self.visit_expr(&gen.iter);
437                    for if_expr in &gen.ifs {
438                        self.visit_expr(if_expr);
439                    }
440                }
441            }
442            ast::Expr::GeneratorExp(comp) => {
443                self.visit_expr(&comp.elt);
444                for gen in &comp.generators {
445                    self.visit_expr(&gen.iter);
446                    for if_expr in &gen.ifs {
447                        self.visit_expr(if_expr);
448                    }
449                }
450            }
451            ast::Expr::Lambda(lambda) => {
452                self.visit_expr(&lambda.body);
453            }
454            ast::Expr::Subscript(sub) => {
455                self.visit_expr(&sub.value);
456                self.visit_expr(&sub.slice);
457            }
458            ast::Expr::Compare(comp) => {
459                self.visit_expr(&comp.left);
460                for comparator in &comp.comparators {
461                    self.visit_expr(comparator);
462                }
463            }
464            ast::Expr::NamedExpr(named) => {
465                tracing::debug!(
466                    "Visiting NamedExpr, value: {:?}",
467                    self.expr_to_string(&named.value)
468                );
469                self.visit_expr(&named.target);
470                self.visit_expr(&named.value);
471            }
472            ast::Expr::Await(await_expr) => {
473                tracing::debug!("Visiting await expression");
474
475                // Check if the inner expression is a call that needs replacement
476                if let ast::Expr::Call(call) = &*await_expr.value {
477                    // Check if this call needs replacement
478                    if let Some(replacement_str) =
479                        self.check_function_call_direct(&call.func, &call.args, &call.keywords)
480                    {
481                        // The replacement already includes await if needed
482                        // Replace the entire await expression with the replacement
483                        let range = expr.range();
484                        let source_range = range.start().to_usize()..range.end().to_usize();
485                        self.replacements.push((source_range, replacement_str));
486                        return;
487                    }
488                }
489
490                // Otherwise, visit the inner expression normally
491                self.visit_expr(&await_expr.value);
492            }
493            _ => {}
494        }
495    }
496
497    /// Check if this is a magic method call and return replacement string directly
498    fn check_magic_method_call_direct(
499        &mut self,
500        _call_expr: &ast::Expr,
501        call: &ast::ExprCall,
502    ) -> Option<String> {
503        // Check if it's a builtin function call with exactly one argument
504        if call.args.len() == 1 {
505            if let ast::Expr::Name(name) = &*call.func {
506                let builtin_name = name.id.as_str();
507                tracing::debug!("Checking builtin function: {}", builtin_name);
508
509                let magic_method = match builtin_name {
510                    "int" => "__int__",
511                    "str" => "__str__",
512                    "float" => "__float__",
513                    "bool" => "__bool__",
514                    "len" => "__len__",
515                    "abs" => "__abs__",
516                    "hash" => "__hash__",
517                    "bytes" => "__bytes__",
518                    "format" => "__format__",
519                    "repr" => "__repr__",
520                    _ => {
521                        tracing::debug!("Unknown builtin: {}", builtin_name);
522                        return None;
523                    }
524                };
525                tracing::debug!("Matched magic method: {}", magic_method);
526
527                // Get the type of the argument using type introspection
528                let type_name = match self.get_expression_type(&call.args[0]) {
529                    Ok(Some(t)) => t,
530                    Ok(None) => {
531                        tracing::debug!(
532                            "No type information available for {} expression",
533                            builtin_name
534                        );
535                        return None;
536                    }
537                    Err(e) => {
538                        tracing::debug!(
539                            "Failed to get type for {} expression: {}",
540                            builtin_name,
541                            e
542                        );
543                        return None;
544                    }
545                };
546
547                tracing::debug!("Type of expression for {}: {}", builtin_name, type_name);
548                // println!("DEBUG: Type of '{}' is '{}'", self.expr_to_string(&call.args[0]), type_name);
549
550                // Look up the replacement for this type's magic method
551                let method_key = format!("{}.{}", type_name, magic_method);
552                let method_key_with_module = if !type_name.contains('.') {
553                    Some(format!(
554                        "{}.{}.{}",
555                        self.module_name, type_name, magic_method
556                    ))
557                } else {
558                    None
559                };
560
561                let replace_info = self.replacements_info.get(&method_key).or_else(|| {
562                    method_key_with_module
563                        .as_ref()
564                        .and_then(|k| self.replacements_info.get(k))
565                });
566
567                if let Some(info) = replace_info {
568                    tracing::debug!(
569                        "Found replacement info for {}: {}",
570                        method_key,
571                        info.replacement_expr
572                    );
573
574                    // Use AST-based replacement by walking the AST directly
575                    let replacement_ast = info
576                        .replacement_ast
577                        .as_ref()
578                        .expect("replacement_ast must be present for proper transformation");
579
580                    // Walk the replacement AST directly with parameter substitution
581                    // Extract base module name from the replacement info
582                    // For "module.Class.method", we want just "module"
583                    let source_module = info
584                        .old_name
585                        .find('.')
586                        .map(|first_dot_pos| &info.old_name[..first_dot_pos]);
587
588                    let replacement = self.walk_replacement_ast(
589                        replacement_ast,
590                        &call.args,
591                        &call.keywords,
592                        &info.parameters,
593                        source_module,
594                    );
595
596                    // Strip module prefix if we're replacing within the same module
597                    let replacement = if let Some(src_mod) = source_module {
598                        if src_mod == self.module_name {
599                            // Strip "module." prefix from the replacement
600                            replacement.replace(&format!("{}.", src_mod), "")
601                        } else {
602                            replacement
603                        }
604                    } else {
605                        replacement
606                    };
607
608                    // For magic methods, check if we should unwrap the builtin call
609                    // Walk the replacement AST to decide based on structure, not string patterns
610                    if self.should_unwrap_magic_method_call(replacement_ast, builtin_name) {
611                        // Extract the inner content by walking the AST
612                        if let rustpython_ast::Expr::Call(builtin_call) = replacement_ast.as_ref() {
613                            if builtin_call.args.len() == 1 {
614                                // Walk the inner expression with the ORIGINAL call's args, not the builtin's
615                                let inner_replacement = self.walk_replacement_ast_impl(
616                                    &builtin_call.args[0],
617                                    &call.args,
618                                    &call.keywords,
619                                    &info.parameters,
620                                    source_module,
621                                );
622                                return Some(inner_replacement);
623                            }
624                        }
625                    }
626
627                    return Some(replacement);
628                } else {
629                    tracing::debug!("No replacement found for {}", method_key);
630                    return None;
631                }
632            }
633        }
634        None
635    }
636
637    /// Check if this is a regular function call that needs replacement (returns string directly)
638    fn check_function_call_direct(
639        &mut self,
640        func: &ast::Expr,
641        args: &[ast::Expr],
642        keywords: &[ast::Keyword],
643    ) -> Option<String> {
644        // Get the function name
645        let func_name = self.get_func_name(func)?;
646        println!(
647            "DEBUG: check_function_call_direct - func_name={}",
648            func_name
649        );
650
651        // Check if this function needs replacement
652        let replace_info = self.replacements_info.get(&func_name);
653        if replace_info.is_none() {
654            println!("DEBUG: No replacement found for {}", func_name);
655            println!(
656                "DEBUG: Available replacements: {:?}",
657                self.replacements_info.keys().collect::<Vec<_>>()
658            );
659            return None;
660        }
661        let replace_info = replace_info.unwrap();
662
663        // If we have an AST for the replacement, use it for proper handling
664        if let Some(ref replacement_ast) = replace_info.replacement_ast {
665            // Build a map of parameters to actual arguments
666            let mut param_map = HashMap::new();
667
668            // Check if this is a method call based on both syntax and construct type
669            // Static methods are NOT method calls - they don't have implicit self/cls
670            let is_method_call = matches!(func, ast::Expr::Attribute(_)) &&
671                matches!(replace_info.construct_type,
672                    crate::core::ConstructType::ClassMethod |
673                    crate::core::ConstructType::Function |
674                    crate::core::ConstructType::AsyncFunction
675                ) &&
676                // Additional check: if it's a regular Function in an attribute context,
677                // check if it's actually a method by looking at the parameters
678                (replace_info.construct_type != crate::core::ConstructType::Function ||
679                 replace_info.parameters.first().is_some_and(|p| p.name == "self"));
680
681            if is_method_call {
682                // For method calls, we need to handle different types
683                if let ast::Expr::Attribute(attr_expr) = func {
684                    match replace_info.construct_type {
685                        crate::core::ConstructType::ClassMethod => {
686                            // For classmethods, first parameter receives the class itself
687                            if let Some(first_param) = replace_info.parameters.first() {
688                                if !first_param.is_vararg && !first_param.is_kwarg {
689                                    param_map.insert(first_param.name.clone(), &*attr_expr.value);
690                                }
691                            }
692
693                            // Map other arguments (skipping first parameter)
694                            for (i, arg) in args.iter().enumerate() {
695                                if let Some(param) = replace_info.parameters.get(i + 1) {
696                                    if !param.is_vararg && !param.is_kwarg {
697                                        param_map.insert(param.name.clone(), arg);
698                                    }
699                                }
700                            }
701                        }
702                        crate::core::ConstructType::StaticMethod => {
703                            // Static methods don't have implicit self/cls
704                            for (i, arg) in args.iter().enumerate() {
705                                if let Some(param) = replace_info.parameters.get(i) {
706                                    if !param.is_vararg && !param.is_kwarg {
707                                        param_map.insert(param.name.clone(), arg);
708                                    }
709                                }
710                            }
711                        }
712                        _ => {
713                            // Regular instance methods - first parameter receives the instance
714                            if let Some(first_param) = replace_info.parameters.first() {
715                                if !first_param.is_vararg && !first_param.is_kwarg {
716                                    param_map.insert(first_param.name.clone(), &*attr_expr.value);
717                                }
718                            }
719
720                            // Map other arguments (skipping first parameter)
721                            for (i, arg) in args.iter().enumerate() {
722                                if let Some(param) = replace_info.parameters.get(i + 1) {
723                                    if !param.is_vararg && !param.is_kwarg {
724                                        param_map.insert(param.name.clone(), arg);
725                                    }
726                                }
727                            }
728                        }
729                    }
730                }
731            } else {
732                // For regular functions, handle *args and **kwargs specially
733                let mut regular_params = Vec::new();
734                let mut vararg_param = None;
735                let mut kwarg_param = None;
736
737                for param in &replace_info.parameters {
738                    if param.is_vararg {
739                        vararg_param = Some(param.name.clone());
740                    } else if param.is_kwarg {
741                        kwarg_param = Some(param.name.clone());
742                    } else {
743                        regular_params.push(param);
744                    }
745                }
746
747                // Check if we have dict unpacking but no **kwargs in replacement
748                let has_dict_unpack = keywords.iter().any(|kw| kw.arg.is_none());
749                let has_kwarg_param = kwarg_param.is_some();
750
751                if has_dict_unpack && !has_kwarg_param {
752                    // Special case: preserve dict unpacking when replacement doesn't have **kwargs
753                    // Just map positional args
754                    for (i, arg) in args.iter().enumerate() {
755                        if i < regular_params.len() {
756                            param_map.insert(regular_params[i].name.clone(), arg);
757                        }
758                    }
759                    // Don't map the dict unpacking - we'll preserve it as-is
760                } else {
761                    // Map positional arguments
762                    for (i, arg) in args.iter().enumerate() {
763                        if i < regular_params.len() {
764                            param_map.insert(regular_params[i].name.clone(), arg);
765                        } else if vararg_param.is_some() {
766                            // Extra positional args go to *args - we'll handle this below
767                            break;
768                        }
769                    }
770
771                    // Map keyword arguments
772                    for keyword in keywords {
773                        if let Some(arg_name) = &keyword.arg {
774                            param_map.insert(arg_name.to_string(), &keyword.value);
775                        }
776                    }
777                }
778            }
779
780            // Transform the replacement AST by walking it directly
781            // For method calls, we need to build the proper argument list that includes self
782            let mut augmented_args = Vec::new();
783            let mut augmented_keywords = Vec::new();
784
785            if is_method_call {
786                // For method calls, add the instance as the first argument
787                if let ast::Expr::Attribute(attr_expr) = func {
788                    augmented_args.push((*attr_expr.value).clone());
789                }
790                // Add the regular arguments
791                augmented_args.extend_from_slice(args);
792            } else {
793                // For regular function calls, use arguments as-is
794                augmented_args.extend_from_slice(args);
795            }
796
797            // Keywords remain the same for both cases
798            augmented_keywords.extend_from_slice(keywords);
799
800            // Extract base module name from the replacement info
801            // For "module.Class.method", we want just "module"
802            let source_module = replace_info
803                .old_name
804                .find('.')
805                .map(|first_dot_pos| &replace_info.old_name[..first_dot_pos]);
806
807            let replacement_str = self.walk_replacement_ast(
808                replacement_ast,
809                &augmented_args,
810                &augmented_keywords,
811                &replace_info.parameters,
812                source_module,
813            );
814
815            // Strip module prefix if we're replacing within the same module
816            let replacement_str = if let Some(src_mod) = source_module {
817                if src_mod == self.module_name {
818                    // Strip "module." prefix from the replacement
819                    replacement_str.replace(&format!("{}.", src_mod), "")
820                } else {
821                    replacement_str
822                }
823            } else {
824                replacement_str
825            };
826
827            return Some(replacement_str);
828        }
829
830        // Fall back to string-based replacement if no AST
831        // Build argument strings for parameter substitution
832        let mut arg_map = HashMap::new();
833
834        // Check if this is a method call (func is an Attribute expression)
835        let is_method_call = matches!(func, ast::Expr::Attribute(_));
836
837        if is_method_call {
838            // For method calls, extract the base object for 'self'
839            if let ast::Expr::Attribute(attr_expr) = func {
840                let self_str = self.expr_to_string(&attr_expr.value);
841                arg_map.insert("self".to_string(), self_str);
842
843                // Map the actual arguments to parameters (skipping 'self')
844                let method_params: Vec<_> = replace_info
845                    .parameters
846                    .iter()
847                    .filter(|p| p.name != "self")
848                    .collect();
849
850                for (i, arg) in args.iter().enumerate() {
851                    if i < method_params.len() {
852                        let param = method_params[i];
853                        let arg_str = self.expr_to_string(arg);
854                        arg_map.insert(param.name.clone(), arg_str);
855                    }
856                }
857            }
858        } else {
859            // For regular function calls, map arguments normally
860            for (i, arg) in args.iter().enumerate() {
861                if i < replace_info.parameters.len() {
862                    let param = &replace_info.parameters[i];
863                    let arg_str = self.expr_to_string(arg);
864                    arg_map.insert(param.name.clone(), arg_str);
865                }
866            }
867        }
868
869        // Map keyword arguments
870        for keyword in keywords {
871            if let Some(arg_name) = &keyword.arg {
872                let arg_str = self.expr_to_string(&keyword.value);
873                arg_map.insert(arg_name.to_string(), arg_str);
874            }
875        }
876
877        // Perform parameter substitution in the replacement expression
878        let mut replacement = replace_info.replacement_expr.clone();
879        for (param_name, arg_value) in arg_map {
880            let placeholder = format!("{{{}}}", param_name);
881            replacement = replacement.replace(&placeholder, &arg_value);
882        }
883
884        Some(replacement)
885    }
886
887    /// Get the fully qualified name of a function
888    fn get_func_name(&mut self, func: &ast::Expr) -> Option<String> {
889        match func {
890            ast::Expr::Name(name) => {
891                // Check if it's imported
892                if let Some(full_name) = self.import_map.get(name.id.as_str()) {
893                    Some(full_name.clone())
894                } else {
895                    // For local functions, try with module prefix first
896                    let full_name = format!("{}.{}", self.module_name, name.id);
897                    #[allow(clippy::map_entry)]
898                    // This is conditional lookup, not contains_key + insert
899                    if self.replacements_info.contains_key(&full_name) {
900                        Some(full_name)
901                    } else {
902                        Some(name.id.to_string())
903                    }
904                }
905            }
906            ast::Expr::Attribute(attr_expr) => {
907                // First check if this is a module.function call
908                let base_str = self.expr_to_string(&attr_expr.value);
909
910                // Resolve base through import map if it's an imported module
911                let resolved_base = if let Some(imported_path) = self.import_map.get(&base_str) {
912                    imported_path.clone()
913                } else {
914                    base_str.clone()
915                };
916
917                let full_path = format!("{}.{}", resolved_base, attr_expr.attr);
918
919                // Check if this full path needs replacement (e.g., library.utils.old_function)
920                if self.replacements_info.contains_key(&full_path) {
921                    tracing::debug!("Found replacement for module function: {}", full_path);
922                    return Some(full_path);
923                }
924
925                // For method calls, we need to resolve the type of the base object
926                // Use type introspection to get the actual type
927                tracing::debug!(
928                    "Attempting to get type for attribute base: {:?}",
929                    self.expr_to_string(&attr_expr.value)
930                );
931                let type_result = self.get_expression_type(&attr_expr.value);
932                println!("DEBUG: get_expression_type result: {:?}", type_result);
933                if let Ok(Some(base_type)) = type_result {
934                    tracing::debug!("Got type for attribute base: {}", base_type);
935
936                    // Check if this type was imported - if so, use the full import path
937                    let full_type = if let Some(imported_path) = self.import_map.get(&base_type) {
938                        tracing::debug!("Type {} was imported as {}", base_type, imported_path);
939                        imported_path.clone()
940                    } else if base_type.contains('.') {
941                        // Already fully qualified
942                        base_type.clone()
943                    } else {
944                        // Local class, use current module
945                        let result = format!("{}.{}", self.module_name, base_type);
946                        result
947                    };
948
949                    let full_method_name = format!("{}.{}", full_type, attr_expr.attr);
950                    tracing::debug!("Checking for method replacement: {}", full_method_name);
951
952                    if self.replacements_info.contains_key(&full_method_name) {
953                        tracing::debug!("Found replacement for method: {}", full_method_name);
954                        return Some(full_method_name);
955                    }
956
957                    // Check parent classes if not found directly
958                    tracing::debug!(
959                        "Checking inheritance map for {}: {:?}",
960                        full_type,
961                        self.inheritance_map.get(&full_type)
962                    );
963
964                    // Try to find inheritance entry - try both the resolved import path and the full module path
965                    let mut parent_classes = self.inheritance_map.get(&full_type);
966
967                    // If not found and this looks like a relative import, try to find the full module path
968                    if parent_classes.is_none() && !full_type.starts_with("dulwich.") {
969                        // Check if there's a corresponding entry with the full module path
970                        for (full_class_name, parents) in &self.inheritance_map {
971                            // Check if this full class name ends with our relative type
972                            if full_class_name
973                                .ends_with(&format!(".{}", full_type.replace("repo.", "")))
974                            {
975                                parent_classes = Some(parents);
976                                break;
977                            }
978                        }
979                    }
980
981                    if let Some(parent_classes) = parent_classes {
982                        for parent in parent_classes {
983                            // parent already includes module prefix (e.g., "test_module.BaseRepo")
984                            let parent_method_name = format!("{}.{}", parent, attr_expr.attr);
985                            tracing::debug!("Checking parent class method: {}", parent_method_name);
986                            if self.replacements_info.contains_key(&parent_method_name) {
987                                tracing::debug!(
988                                    "Found replacement in parent class: {}",
989                                    parent_method_name
990                                );
991                                return Some(parent_method_name);
992                            }
993                        }
994                    }
995
996                    // Also try without module prefix in case the type already includes it
997                    let method_name = format!("{}.{}", base_type, attr_expr.attr);
998                    if self.replacements_info.contains_key(&method_name) {
999                        return Some(method_name);
1000                    }
1001                }
1002
1003                // Return the literal path
1004                Some(full_path)
1005            }
1006            _ => None,
1007        }
1008    }
1009
1010    /// Get the type of an expression
1011    fn get_expression_type(
1012        &mut self,
1013        expr: &ast::Expr,
1014    ) -> Result<Option<String>, TypeIntrospectionError> {
1015        // Get the range of the expression from the AST
1016        let range = expr.range();
1017        let start_byte = range.start().to_usize();
1018        let end_byte = range.end().to_usize();
1019
1020        // For attribute expressions, query at the exact position of the attribute name
1021        let query_byte = match expr {
1022            ast::Expr::Attribute(_attr_expr) => {
1023                // For attribute access like `obj.attr`, we want to query at the position of `attr`
1024                // Find the position of the dot and query just after it
1025                let expr_text =
1026                    &self.source_content[start_byte..end_byte.min(self.source_content.len())];
1027                if let Some(dot_pos) = expr_text.rfind('.') {
1028                    // Position right after the dot, at the start of the attribute name
1029                    start_byte + dot_pos + 1
1030                } else {
1031                    // Fallback to end of expression if no dot found
1032                    end_byte.saturating_sub(1)
1033                }
1034            }
1035            _ => start_byte,
1036        };
1037
1038        // Validate byte position
1039        if query_byte > self.source_content.len() {
1040            return Err(TypeIntrospectionError::PositionError(format!(
1041                "Byte position {} exceeds source length {}",
1042                query_byte,
1043                self.source_content.len()
1044            )));
1045        }
1046
1047        // Calculate line and column from byte position
1048        let line = self.source_content[..query_byte].matches('\n').count() + 1;
1049        let line_start = self.source_content[..query_byte]
1050            .rfind('\n')
1051            .map(|p| p + 1)
1052            .unwrap_or(0);
1053        let column = query_byte - line_start;
1054
1055        let position = (line as u32, column as u32);
1056
1057        // Debug: show position and text at that position (comment out for production)
1058        // let expr_end = end_byte.min(self.source_content.len());
1059        // let expr_text = &self.source_content[start_byte..expr_end];
1060        // eprintln!("DEBUG: Querying type at position {}:{} (byte {}) for expr: '{}'", line, column, query_byte, expr_text);
1061
1062        // Check cache first
1063        if let Some(cached) = self.type_cache.borrow().get(&position) {
1064            return Ok(cached.clone());
1065        }
1066
1067        // Use type introspection based on the configured method
1068        let result = match self.type_introspection {
1069            TypeIntrospectionMethod::PyrightLsp => {
1070                if let Some(pyright) = &self.pyright_client {
1071                    match pyright.borrow_mut().query_type(
1072                        &self.file_path,
1073                        &self.source_content,
1074                        line as u32,
1075                        column as u32,
1076                    ) {
1077                        Ok(type_opt) => type_opt,
1078                        Err(e) => {
1079                            tracing::warn!(
1080                                "Pyright type query failed at {}:{}: {}",
1081                                line,
1082                                column,
1083                                e
1084                            );
1085                            return Err(TypeIntrospectionError::PyrightError(e.to_string()));
1086                        }
1087                    }
1088                } else {
1089                    return Err(TypeIntrospectionError::NoClientAvailable);
1090                }
1091            }
1092            TypeIntrospectionMethod::MypyDaemon => {
1093                if let Some(mypy) = &self.mypy_client {
1094                    match mypy
1095                        .borrow_mut()
1096                        .get_type_at_position(&self.file_path, line, column + 1) // dmypy expects 1-based columns
1097                    {
1098                        Ok(type_opt) => type_opt,
1099                        Err(e) => {
1100                            tracing::warn!("Mypy type query failed at {}:{}: {}", line, column, e);
1101                            return Err(TypeIntrospectionError::MypyError(e));
1102                        }
1103                    }
1104                } else {
1105                    return Err(TypeIntrospectionError::NoClientAvailable);
1106                }
1107            }
1108            TypeIntrospectionMethod::PyrightWithMypyFallback => {
1109                // Try Pyright first
1110                let pyright_result = if let Some(pyright) = &self.pyright_client {
1111                    match pyright.borrow_mut().query_type(
1112                        &self.file_path,
1113                        &self.source_content,
1114                        line as u32,
1115                        column as u32,
1116                    ) {
1117                        Ok(type_opt) => type_opt,
1118                        Err(e) => {
1119                            tracing::debug!("Pyright failed, trying Mypy: {}", e);
1120                            None
1121                        }
1122                    }
1123                } else {
1124                    None
1125                };
1126
1127                if pyright_result.is_some() {
1128                    pyright_result
1129                } else if let Some(mypy) = &self.mypy_client {
1130                    match mypy
1131                        .borrow_mut()
1132                        .get_type_at_position(&self.file_path, line, column + 1) // dmypy expects 1-based columns
1133                    {
1134                        Ok(type_opt) => type_opt,
1135                        Err(e) => {
1136                            tracing::warn!(
1137                                "Both Pyright and Mypy failed at {}:{}: {}",
1138                                line,
1139                                column,
1140                                e
1141                            );
1142                            return Err(TypeIntrospectionError::MypyError(e));
1143                        }
1144                    }
1145                } else {
1146                    return Err(TypeIntrospectionError::NoClientAvailable);
1147                }
1148            }
1149        };
1150
1151        // Clean up the type string if needed
1152        let cleaned_result = result.map(|t| {
1153            // Remove module prefixes if present for simpler matching
1154            if let Some(last_dot) = t.rfind('.') {
1155                t[last_dot + 1..].to_string()
1156            } else {
1157                t
1158            }
1159        });
1160
1161        // Cache the result
1162        self.type_cache
1163            .borrow_mut()
1164            .insert(position, cleaned_result.clone());
1165
1166        Ok(cleaned_result)
1167    }
1168
1169    /// Convert an expression to a string
1170    fn expr_to_string(&self, expr: &ast::Expr) -> String {
1171        // Reconstruct the string from the AST
1172        self.expr_to_string_with_placeholders(expr, &HashSet::new())
1173    }
1174
1175    /// Convert an expression to a string with placeholders
1176    #[allow(clippy::only_used_in_recursion)] // self is needed for recursive method calls
1177    fn expr_to_string_with_placeholders<R>(
1178        &self,
1179        expr: &rustpython_ast::Expr<R>,
1180        param_names: &HashSet<String>,
1181    ) -> String {
1182        match expr {
1183            rustpython_ast::Expr::Name(name) => {
1184                ast_utils::format_name_with_placeholders(&name.id, param_names)
1185            }
1186            rustpython_ast::Expr::Constant(c) => ast_utils::format_constant(&c.value),
1187            rustpython_ast::Expr::Call(call) => {
1188                let func_str = self.expr_to_string_with_placeholders(&*call.func, param_names);
1189                let mut arg_strs = Vec::new();
1190                for arg in &call.args {
1191                    arg_strs.push(self.expr_to_string_with_placeholders(arg, param_names));
1192                }
1193                for keyword in &call.keywords {
1194                    if let Some(arg) = &keyword.arg {
1195                        arg_strs.push(format!(
1196                            "{}={}",
1197                            arg,
1198                            self.expr_to_string_with_placeholders(&keyword.value, param_names)
1199                        ));
1200                    }
1201                }
1202                format!("{}({})", func_str, arg_strs.join(", "))
1203            }
1204            rustpython_ast::Expr::Attribute(attr) => {
1205                let value_str = self.expr_to_string_with_placeholders(&*attr.value, param_names);
1206                format!("{}.{}", value_str, attr.attr)
1207            }
1208            rustpython_ast::Expr::BinOp(binop) => {
1209                let left = self.expr_to_string_with_placeholders(&*binop.left, param_names);
1210                let right = self.expr_to_string_with_placeholders(&*binop.right, param_names);
1211                let op = match &binop.op {
1212                    rustpython_ast::Operator::Add => "+",
1213                    rustpython_ast::Operator::Sub => "-",
1214                    rustpython_ast::Operator::Mult => "*",
1215                    rustpython_ast::Operator::Div => "/",
1216                    rustpython_ast::Operator::Mod => "%",
1217                    rustpython_ast::Operator::Pow => "**",
1218                    rustpython_ast::Operator::FloorDiv => "//",
1219                    rustpython_ast::Operator::LShift => "<<",
1220                    rustpython_ast::Operator::RShift => ">>",
1221                    rustpython_ast::Operator::BitOr => "|",
1222                    rustpython_ast::Operator::BitXor => "^",
1223                    rustpython_ast::Operator::BitAnd => "&",
1224                    rustpython_ast::Operator::MatMult => "@",
1225                };
1226                format!("{} {} {}", left, op, right)
1227            }
1228            rustpython_ast::Expr::BoolOp(boolop) => {
1229                let values: Vec<String> = boolop
1230                    .values
1231                    .iter()
1232                    .map(|v| self.expr_to_string_with_placeholders(v, param_names))
1233                    .collect();
1234                let op = match &boolop.op {
1235                    rustpython_ast::BoolOp::And => " and ",
1236                    rustpython_ast::BoolOp::Or => " or ",
1237                };
1238                values.join(op)
1239            }
1240            rustpython_ast::Expr::UnaryOp(unaryop) => {
1241                let operand = self.expr_to_string_with_placeholders(&*unaryop.operand, param_names);
1242                let op = match &unaryop.op {
1243                    rustpython_ast::UnaryOp::Not => "not ",
1244                    rustpython_ast::UnaryOp::UAdd => "+",
1245                    rustpython_ast::UnaryOp::USub => "-",
1246                    rustpython_ast::UnaryOp::Invert => "~",
1247                };
1248                format!("{}{}", op, operand)
1249            }
1250            rustpython_ast::Expr::Compare(compare) => {
1251                let mut result = self.expr_to_string_with_placeholders(&*compare.left, param_names);
1252                for (op, comparator) in compare.ops.iter().zip(&compare.comparators) {
1253                    let op_str = match op {
1254                        rustpython_ast::CmpOp::Eq => " == ",
1255                        rustpython_ast::CmpOp::NotEq => " != ",
1256                        rustpython_ast::CmpOp::Lt => " < ",
1257                        rustpython_ast::CmpOp::LtE => " <= ",
1258                        rustpython_ast::CmpOp::Gt => " > ",
1259                        rustpython_ast::CmpOp::GtE => " >= ",
1260                        rustpython_ast::CmpOp::Is => " is ",
1261                        rustpython_ast::CmpOp::IsNot => " is not ",
1262                        rustpython_ast::CmpOp::In => " in ",
1263                        rustpython_ast::CmpOp::NotIn => " not in ",
1264                    };
1265                    result.push_str(op_str);
1266                    result
1267                        .push_str(&self.expr_to_string_with_placeholders(comparator, param_names));
1268                }
1269                result
1270            }
1271            rustpython_ast::Expr::JoinedStr(joined) => {
1272                // F-string
1273                let mut result = String::from("f\"");
1274                for value in &joined.values {
1275                    match value {
1276                        rustpython_ast::Expr::Constant(c) => {
1277                            if let ast::Constant::Str(s) = &c.value {
1278                                result.push_str(s);
1279                            }
1280                        }
1281                        rustpython_ast::Expr::FormattedValue(fval) => {
1282                            result.push('{');
1283                            result.push_str(
1284                                &self.expr_to_string_with_placeholders(&*fval.value, param_names),
1285                            );
1286                            if let Some(spec) = &fval.format_spec {
1287                                result.push(':');
1288                                // Format spec is itself a JoinedStr expression
1289                                if let rustpython_ast::Expr::JoinedStr(spec_joined) = &**spec {
1290                                    for spec_part in &spec_joined.values {
1291                                        if let rustpython_ast::Expr::Constant(c) = spec_part {
1292                                            if let ast::Constant::Str(s) = &c.value {
1293                                                result.push_str(s);
1294                                            }
1295                                        }
1296                                    }
1297                                }
1298                            }
1299                            result.push('}');
1300                        }
1301                        _ => {}
1302                    }
1303                }
1304                result.push('"');
1305                result
1306            }
1307            rustpython_ast::Expr::List(list) => {
1308                let items: Vec<String> = list
1309                    .elts
1310                    .iter()
1311                    .map(|e| self.expr_to_string_with_placeholders(e, param_names))
1312                    .collect();
1313                format!("[{}]", items.join(", "))
1314            }
1315            rustpython_ast::Expr::Tuple(tuple) => {
1316                let items: Vec<String> = tuple
1317                    .elts
1318                    .iter()
1319                    .map(|e| self.expr_to_string_with_placeholders(e, param_names))
1320                    .collect();
1321                if items.len() == 1 {
1322                    format!("({},)", items[0])
1323                } else {
1324                    format!("({})", items.join(", "))
1325                }
1326            }
1327            rustpython_ast::Expr::Dict(dict) => {
1328                let mut items = Vec::new();
1329                for (key_opt, value) in dict.keys.iter().zip(&dict.values) {
1330                    if let Some(key) = key_opt {
1331                        let key_str = self.expr_to_string_with_placeholders(key, param_names);
1332                        let value_str = self.expr_to_string_with_placeholders(value, param_names);
1333                        items.push(format!("{}: {}", key_str, value_str));
1334                    } else {
1335                        // None key means dictionary unpacking
1336                        let value_str = self.expr_to_string_with_placeholders(value, param_names);
1337                        items.push(format!("**{}", value_str));
1338                    }
1339                }
1340                format!("{{{}}}", items.join(", "))
1341            }
1342            rustpython_ast::Expr::Set(set) => {
1343                let items: Vec<String> = set
1344                    .elts
1345                    .iter()
1346                    .map(|e| self.expr_to_string_with_placeholders(e, param_names))
1347                    .collect();
1348                format!("{{{}}}", items.join(", "))
1349            }
1350            rustpython_ast::Expr::Subscript(sub) => {
1351                let value = self.expr_to_string_with_placeholders(&*sub.value, param_names);
1352                let slice = self.expr_to_string_with_placeholders(&*sub.slice, param_names);
1353                format!("{}[{}]", value, slice)
1354            }
1355            rustpython_ast::Expr::Slice(slice) => {
1356                let lower = slice
1357                    .lower
1358                    .as_ref()
1359                    .map(|e| self.expr_to_string_with_placeholders(&**e, param_names))
1360                    .unwrap_or_default();
1361                let upper = slice
1362                    .upper
1363                    .as_ref()
1364                    .map(|e| self.expr_to_string_with_placeholders(&**e, param_names))
1365                    .unwrap_or_default();
1366                let step = slice
1367                    .step
1368                    .as_ref()
1369                    .map(|e| {
1370                        format!(
1371                            ":{}",
1372                            self.expr_to_string_with_placeholders(&**e, param_names)
1373                        )
1374                    })
1375                    .unwrap_or_default();
1376                format!("{}:{}{}", lower, upper, step)
1377            }
1378            rustpython_ast::Expr::Lambda(lambda) => {
1379                let args: Vec<String> = lambda
1380                    .args
1381                    .args
1382                    .iter()
1383                    .map(|arg| arg.def.arg.to_string())
1384                    .collect();
1385                let body = self.expr_to_string_with_placeholders(&*lambda.body, param_names);
1386                if args.is_empty() {
1387                    format!("lambda: {}", body)
1388                } else {
1389                    format!("lambda {}: {}", args.join(", "), body)
1390                }
1391            }
1392            rustpython_ast::Expr::ListComp(comp) => {
1393                let elt = self.expr_to_string_with_placeholders(&*comp.elt, param_names);
1394                let mut comp_str = String::new();
1395                for gen in &comp.generators {
1396                    let target = self.expr_to_string_with_placeholders(&gen.target, param_names);
1397                    let iter = self.expr_to_string_with_placeholders(&gen.iter, param_names);
1398                    let async_keyword = if gen.is_async { "async " } else { "" };
1399                    comp_str.push_str(&format!(" {}for {} in {}", async_keyword, target, iter));
1400                    for if_clause in &gen.ifs {
1401                        let if_expr = self.expr_to_string_with_placeholders(if_clause, param_names);
1402                        comp_str.push_str(&format!(" if {}", if_expr));
1403                    }
1404                }
1405                format!("[{}{}]", elt, comp_str)
1406            }
1407            rustpython_ast::Expr::SetComp(comp) => {
1408                let elt = self.expr_to_string_with_placeholders(&*comp.elt, param_names);
1409                let mut comp_str = String::new();
1410                for gen in &comp.generators {
1411                    let target = self.expr_to_string_with_placeholders(&gen.target, param_names);
1412                    let iter = self.expr_to_string_with_placeholders(&gen.iter, param_names);
1413                    let async_keyword = if gen.is_async { "async " } else { "" };
1414                    comp_str.push_str(&format!(" {}for {} in {}", async_keyword, target, iter));
1415                    for if_clause in &gen.ifs {
1416                        let if_expr = self.expr_to_string_with_placeholders(if_clause, param_names);
1417                        comp_str.push_str(&format!(" if {}", if_expr));
1418                    }
1419                }
1420                format!("{{{}{}}}", elt, comp_str)
1421            }
1422            rustpython_ast::Expr::DictComp(comp) => {
1423                let key = self.expr_to_string_with_placeholders(&*comp.key, param_names);
1424                let value = self.expr_to_string_with_placeholders(&*comp.value, param_names);
1425                let mut comp_str = String::new();
1426                for gen in &comp.generators {
1427                    let target = self.expr_to_string_with_placeholders(&gen.target, param_names);
1428                    let iter = self.expr_to_string_with_placeholders(&gen.iter, param_names);
1429                    let async_keyword = if gen.is_async { "async " } else { "" };
1430                    comp_str.push_str(&format!(" {}for {} in {}", async_keyword, target, iter));
1431                    for if_clause in &gen.ifs {
1432                        let if_expr = self.expr_to_string_with_placeholders(if_clause, param_names);
1433                        comp_str.push_str(&format!(" if {}", if_expr));
1434                    }
1435                }
1436                format!("{{{}: {}{}}}", key, value, comp_str)
1437            }
1438            rustpython_ast::Expr::GeneratorExp(comp) => {
1439                let elt = self.expr_to_string_with_placeholders(&*comp.elt, param_names);
1440                let mut comp_str = String::new();
1441                for gen in &comp.generators {
1442                    let target = self.expr_to_string_with_placeholders(&gen.target, param_names);
1443                    let iter = self.expr_to_string_with_placeholders(&gen.iter, param_names);
1444                    let async_keyword = if gen.is_async { "async " } else { "" };
1445                    comp_str.push_str(&format!(" {}for {} in {}", async_keyword, target, iter));
1446                    for if_clause in &gen.ifs {
1447                        let if_expr = self.expr_to_string_with_placeholders(if_clause, param_names);
1448                        comp_str.push_str(&format!(" if {}", if_expr));
1449                    }
1450                }
1451                format!("({}{})", elt, comp_str)
1452            }
1453            rustpython_ast::Expr::IfExp(if_exp) => {
1454                let body = self.expr_to_string_with_placeholders(&*if_exp.body, param_names);
1455                let test = self.expr_to_string_with_placeholders(&*if_exp.test, param_names);
1456                let orelse = self.expr_to_string_with_placeholders(&*if_exp.orelse, param_names);
1457                format!("{} if {} else {}", body, test, orelse)
1458            }
1459            rustpython_ast::Expr::NamedExpr(named) => {
1460                let target = self.expr_to_string_with_placeholders(&*named.target, param_names);
1461                let value = self.expr_to_string_with_placeholders(&*named.value, param_names);
1462                format!("({} := {})", target, value)
1463            }
1464            rustpython_ast::Expr::Starred(starred) => {
1465                let value = self.expr_to_string_with_placeholders(&*starred.value, param_names);
1466                format!("*{}", value)
1467            }
1468            rustpython_ast::Expr::Await(await_expr) => {
1469                let value = self.expr_to_string_with_placeholders(&*await_expr.value, param_names);
1470                format!("await {}", value)
1471            }
1472            _ => "".to_string(),
1473        }
1474    }
1475
1476    /// Transform a replacement AST with parameter substitution, returning a new AST
1477    fn transform_replacement_ast_to_expr(
1478        &self,
1479        replacement_ast: &rustpython_ast::Expr,
1480        obj_expr: &ast::Expr,
1481    ) -> ast::Expr {
1482        // Transform the replacement AST by replacing 'self' with obj_expr
1483        self.transform_expr_replacing_self(replacement_ast, obj_expr)
1484    }
1485
1486    /// Transform a replacement AST with general parameter substitution
1487    fn transform_replacement_ast_with_params(
1488        &self,
1489        replacement_ast: &rustpython_ast::Expr,
1490        param_map: &HashMap<String, &ast::Expr>,
1491    ) -> ast::Expr {
1492        self.transform_expr_with_params(replacement_ast, param_map)
1493    }
1494
1495    /// Recursively transform an expression, replacing parameters with their values
1496    fn transform_expr_with_params(
1497        &self,
1498        expr: &rustpython_ast::Expr,
1499        param_map: &HashMap<String, &ast::Expr>,
1500    ) -> ast::Expr {
1501        match expr {
1502            ast::Expr::Name(name) => {
1503                // Check if this name should be replaced with a parameter value
1504                if let Some(replacement_expr) = param_map.get(name.id.as_str()) {
1505                    (*replacement_expr).clone()
1506                } else {
1507                    ast::Expr::Name(ast::ExprName {
1508                        id: name.id.clone(),
1509                        ctx: ast::ExprContext::Load,
1510                        range: Default::default(),
1511                    })
1512                }
1513            }
1514            ast::Expr::Attribute(attr) => {
1515                // Transform the value part
1516                let value = self.transform_expr_with_params(&attr.value, param_map);
1517                ast::Expr::Attribute(ast::ExprAttribute {
1518                    value: Box::new(value),
1519                    attr: attr.attr.clone(),
1520                    ctx: ast::ExprContext::Load,
1521                    range: Default::default(),
1522                })
1523            }
1524            ast::Expr::Call(call) => {
1525                // Transform function and arguments
1526                let func = self.transform_expr_with_params(&call.func, param_map);
1527                let args: Vec<ast::Expr> = call
1528                    .args
1529                    .iter()
1530                    .map(|arg| self.transform_expr_with_params(arg, param_map))
1531                    .collect();
1532                let keywords: Vec<ast::Keyword> = call
1533                    .keywords
1534                    .iter()
1535                    .map(|kw| ast::Keyword {
1536                        arg: kw.arg.clone(),
1537                        value: self.transform_expr_with_params(&kw.value, param_map),
1538                        range: Default::default(),
1539                    })
1540                    .collect();
1541
1542                ast::Expr::Call(ast::ExprCall {
1543                    func: Box::new(func),
1544                    args,
1545                    keywords,
1546                    range: Default::default(),
1547                })
1548            }
1549            ast::Expr::BinOp(binop) => {
1550                let left = self.transform_expr_with_params(&binop.left, param_map);
1551                let right = self.transform_expr_with_params(&binop.right, param_map);
1552                ast::Expr::BinOp(ast::ExprBinOp {
1553                    left: Box::new(left),
1554                    op: binop.op,
1555                    right: Box::new(right),
1556                    range: Default::default(),
1557                })
1558            }
1559            ast::Expr::Await(await_expr) => {
1560                let value = self.transform_expr_with_params(&await_expr.value, param_map);
1561                ast::Expr::Await(ast::ExprAwait {
1562                    value: Box::new(value),
1563                    range: Default::default(),
1564                })
1565            }
1566            // Handle other expressions by converting them directly
1567            _ => self.convert_expr(expr),
1568        }
1569    }
1570
1571    /// Recursively transform an expression, replacing 'self' with the given expression
1572    fn transform_expr_replacing_self(
1573        &self,
1574        expr: &rustpython_ast::Expr,
1575        obj_expr: &ast::Expr,
1576    ) -> ast::Expr {
1577        match expr {
1578            ast::Expr::Name(name) if name.id.as_str() == "self" => {
1579                // Replace 'self' with the object expression
1580                obj_expr.clone()
1581            }
1582            ast::Expr::Attribute(attr) => {
1583                // Transform the value part
1584                let value = self.transform_expr_replacing_self(&attr.value, obj_expr);
1585                ast::Expr::Attribute(ast::ExprAttribute {
1586                    value: Box::new(value),
1587                    attr: attr.attr.clone(),
1588                    ctx: ast::ExprContext::Load,
1589                    range: Default::default(),
1590                })
1591            }
1592            ast::Expr::Call(call) => {
1593                // Transform function and arguments
1594                let func = self.transform_expr_replacing_self(&call.func, obj_expr);
1595                let args: Vec<ast::Expr> = call
1596                    .args
1597                    .iter()
1598                    .map(|arg| self.transform_expr_replacing_self(arg, obj_expr))
1599                    .collect();
1600                let keywords: Vec<ast::Keyword> = call
1601                    .keywords
1602                    .iter()
1603                    .map(|kw| ast::Keyword {
1604                        arg: kw.arg.clone(),
1605                        value: self.transform_expr_replacing_self(&kw.value, obj_expr),
1606                        range: Default::default(),
1607                    })
1608                    .collect();
1609
1610                ast::Expr::Call(ast::ExprCall {
1611                    func: Box::new(func),
1612                    args,
1613                    keywords,
1614                    range: Default::default(),
1615                })
1616            }
1617            // For other expressions, convert them directly
1618            _ => {
1619                // Convert the expression - now mostly an identity function
1620                // but ensures proper range fields are set
1621                self.convert_expr(expr)
1622            }
1623        }
1624    }
1625
1626    /// Convert an expression to a regular expression (identity function now)
1627    fn convert_expr(&self, expr: &rustpython_ast::Expr) -> ast::Expr {
1628        match expr {
1629            ast::Expr::Name(name) => ast::Expr::Name(ast::ExprName {
1630                id: name.id.clone(),
1631                ctx: ast::ExprContext::Load,
1632                range: Default::default(),
1633            }),
1634            ast::Expr::Constant(c) => ast::Expr::Constant(ast::ExprConstant {
1635                value: c.value.clone(),
1636                kind: c.kind.clone(),
1637                range: Default::default(),
1638            }),
1639            _ => {
1640                // Fallback: parse the string representation
1641                let expr_str = self.expr_to_string_with_placeholders(expr, &HashSet::new());
1642                match rustpython_parser::parse(
1643                    &expr_str,
1644                    rustpython_parser::Mode::Expression,
1645                    "<conversion>",
1646                ) {
1647                    Ok(ast::Mod::Expression(expr_mod)) => *expr_mod.body,
1648                    _ => ast::Expr::Name(ast::ExprName {
1649                        id: "ERROR".to_string().into(),
1650                        ctx: ast::ExprContext::Load,
1651                        range: Default::default(),
1652                    }),
1653                }
1654            }
1655        }
1656    }
1657
1658    /// Transform a replacement AST with actual arguments
1659    fn transform_replacement_ast_with_args(
1660        &self,
1661        ast: &rustpython_ast::Expr,
1662        param_map: &HashMap<String, &ast::Expr>,
1663        all_args: &[ast::Expr],
1664        all_keywords: &[ast::Keyword],
1665        parameters: &[ParameterInfo],
1666    ) -> String {
1667        // Convert the rustpython AST to string, substituting parameters with actual arguments
1668        self.rustpython_expr_to_string_with_args(ast, param_map, all_args, all_keywords, parameters)
1669    }
1670
1671    /// Convert rustpython AST expression to string with argument substitution
1672    fn rustpython_expr_to_string_with_args(
1673        &self,
1674        expr: &rustpython_ast::Expr,
1675        param_map: &HashMap<String, &ast::Expr>,
1676        all_args: &[ast::Expr],
1677        all_keywords: &[ast::Keyword],
1678        parameters: &[ParameterInfo],
1679    ) -> String {
1680        self.rustpython_expr_to_string_with_args_impl(
1681            expr,
1682            param_map,
1683            all_args,
1684            all_keywords,
1685            parameters,
1686            true,
1687        )
1688    }
1689
1690    /// Walk replacement AST directly without template parsing - this is the clean approach
1691    fn walk_replacement_ast(
1692        &self,
1693        expr: &rustpython_ast::Expr,
1694        call_args: &[ast::Expr],
1695        call_keywords: &[ast::Keyword],
1696        parameters: &[ParameterInfo],
1697        source_module: Option<&str>,
1698    ) -> String {
1699        self.walk_replacement_ast_impl(expr, call_args, call_keywords, parameters, source_module)
1700    }
1701
1702    fn walk_replacement_ast_impl(
1703        &self,
1704        expr: &rustpython_ast::Expr,
1705        call_args: &[ast::Expr],
1706        call_keywords: &[ast::Keyword],
1707        parameters: &[ParameterInfo],
1708        source_module: Option<&str>,
1709    ) -> String {
1710        use rustpython_ast as rpy_ast;
1711
1712        match expr {
1713            rpy_ast::Expr::Name(name) => {
1714                let name_str = name.id.as_str();
1715                // Check if this is a vararg parameter - these should NOT be mapped by index
1716                // Note: kwarg parameters are handled separately below
1717                if let Some(_param) = parameters
1718                    .iter()
1719                    .find(|p| p.name == name_str && p.is_vararg)
1720                {
1721                    // For *args, we don't substitute the name itself
1722                    // It will be expanded when we encounter it in a Starred context
1723                    return name_str.to_string();
1724                }
1725
1726                // Map regular parameter names to actual call arguments
1727                if let Some(param_index) = parameters
1728                    .iter()
1729                    .position(|p| p.name == name_str && !p.is_vararg && !p.is_kwarg)
1730                {
1731                    if param_index < call_args.len() {
1732                        return self.expr_to_string(&call_args[param_index]);
1733                    }
1734
1735                    // If we don't have a positional argument at this index,
1736                    // check if there's a starred keyword argument that could fill this parameter.
1737                    // This handles the case where **kwargs should fill a missing positional parameter.
1738                    if param_index == call_args.len() && !call_keywords.is_empty() {
1739                        // Look for the first starred keyword argument (**kwargs style)
1740                        for keyword in call_keywords.iter() {
1741                            if keyword.arg.is_none() {
1742                                // This is a **kwargs expansion - use it for this parameter
1743                                return format!("**{}", self.expr_to_string(&keyword.value));
1744                            }
1745                        }
1746                    }
1747                }
1748
1749                // Check if this is a **kwargs parameter that should collect individual keyword arguments
1750                if let Some(_param) = parameters.iter().find(|p| p.is_kwarg && p.name == name_str) {
1751                    // This is a **kwargs parameter - collect all keyword arguments
1752                    let individual_kwargs: Vec<String> = call_keywords
1753                        .iter()
1754                        .map(|kw| {
1755                            if let Some(arg) = &kw.arg {
1756                                format!("{}={}", arg, self.expr_to_string(&kw.value))
1757                            } else {
1758                                // This is a starred expansion like **dict, preserve it with **
1759                                format!("**{}", self.expr_to_string(&kw.value))
1760                            }
1761                        })
1762                        .collect();
1763
1764                    if !individual_kwargs.is_empty() {
1765                        return individual_kwargs.join(", ");
1766                    } else {
1767                        // No kwargs to expand - return empty string
1768                        return String::new();
1769                    }
1770                }
1771
1772                // Check for keyword arguments with names
1773                if let Some(keyword) = call_keywords
1774                    .iter()
1775                    .find(|kw| kw.arg.as_ref().map(|arg| arg.as_str()) == Some(name_str))
1776                {
1777                    return self.expr_to_string(&keyword.value);
1778                }
1779
1780                // Check if this is a parameter that wasn't provided but has a default value
1781                if let Some(param) = parameters.iter().find(|p| p.name == name_str) {
1782                    if let Some(param_index) = parameters.iter().position(|p| p.name == name_str) {
1783                        let is_provided = param_index < call_args.len()
1784                            || call_keywords.iter().any(|kw| {
1785                                kw.arg.as_ref().map(|arg| arg.as_str()) == Some(name_str)
1786                            });
1787
1788                        if !is_provided && param.has_default {
1789                            // Parameter not provided and has default - use a placeholder that will be filtered out
1790                            return "__OMIT_PARAMETER__".to_string();
1791                        }
1792                    }
1793                }
1794
1795                // If it's not a parameter, check if we need to qualify the name with the source module
1796                if let Some(source_mod) = source_module {
1797                    // Function is not available in current scope
1798                    // Check if it's imported from the source module
1799                    let full_name = format!("{}.{}", source_mod, name_str);
1800                    if self.import_map.values().any(|v| v == &full_name) {
1801                        // It's imported with its full name, use the short name
1802                        name_str.to_string()
1803                    } else {
1804                        // Not imported - use the fully qualified name from source module
1805                        full_name
1806                    }
1807                } else {
1808                    // No source module information, use the name as-is
1809                    name_str.to_string()
1810                }
1811            }
1812
1813            rpy_ast::Expr::Attribute(attr) => {
1814                let value = self.walk_replacement_ast_impl(
1815                    &attr.value,
1816                    call_args,
1817                    call_keywords,
1818                    parameters,
1819                    source_module,
1820                );
1821                format!("{}.{}", value, attr.attr)
1822            }
1823
1824            rpy_ast::Expr::Call(call) => {
1825                // Handle the function name with potential name resolution
1826                let func = match call.func.as_ref() {
1827                    rpy_ast::Expr::Name(name) => {
1828                        let func_name = name.id.as_str();
1829                        // Check if this is a parameter that should be substituted
1830                        if let Some(param_index) =
1831                            parameters.iter().position(|p| p.name == func_name)
1832                        {
1833                            if param_index < call_args.len() {
1834                                self.expr_to_string(&call_args[param_index])
1835                            } else {
1836                                // Apply name resolution for functions that aren't parameters
1837                                if !self.import_map.contains_key(func_name)
1838                                    && !self.builtins.contains(func_name)
1839                                {
1840                                    format!("{}.{}", self.module_name, func_name)
1841                                } else {
1842                                    func_name.to_string()
1843                                }
1844                            }
1845                        } else {
1846                            // Not a parameter - check if this function exists in scope
1847                            // Only apply name resolution if the function is not already available
1848
1849                            // Check if this is imported or a builtin
1850                            if self.import_map.contains_key(func_name)
1851                                || self.builtins.contains(func_name)
1852                            {
1853                                // Function is imported or is a builtin - use it directly
1854                                func_name.to_string()
1855                            } else if self.module_functions.contains(func_name) {
1856                                // Function is defined in this module - use it directly
1857                                func_name.to_string()
1858                            } else if let Some(source_mod) = source_module {
1859                                // Function is not available in current scope
1860                                // Check if it's imported from the source module
1861                                let full_name = format!("{}.{}", source_mod, func_name);
1862                                if self.import_map.values().any(|v| v == &full_name) {
1863                                    // It's imported with its full name, use the short name
1864                                    func_name.to_string()
1865                                } else {
1866                                    // Not imported - use the fully qualified name from source module
1867                                    full_name
1868                                }
1869                            } else {
1870                                // No source module info - use as-is
1871                                func_name.to_string()
1872                            }
1873                        }
1874                    }
1875                    _ => self.walk_replacement_ast_impl(
1876                        &call.func,
1877                        call_args,
1878                        call_keywords,
1879                        parameters,
1880                        source_module,
1881                    ),
1882                };
1883                let args: Vec<String> = call
1884                    .args
1885                    .iter()
1886                    .filter_map(|arg| {
1887                        let value = self.walk_replacement_ast_impl(
1888                            arg,
1889                            call_args,
1890                            call_keywords,
1891                            parameters,
1892                            source_module,
1893                        );
1894
1895                        // Skip empty strings (e.g., from empty *args expansion)
1896                        if value.is_empty() {
1897                            return None;
1898                        }
1899
1900                        // Check if this is an unsubstituted parameter name
1901                        // If so, skip it (don't include unprovided parameters in the call)
1902                        if let rpy_ast::Expr::Name(name) = arg {
1903                            let name_str = name.id.as_str();
1904                            // Only filter out if:
1905                            // 1. The value equals the parameter name (unsubstituted)
1906                            // 2. The parameter exists in the replacement signature
1907                            // 3. AND there's no actual argument provided for this parameter
1908                            if value == name_str && parameters.iter().any(|p| p.name == name_str) {
1909                                // Check if this parameter has a corresponding argument
1910                                if let Some(param_index) =
1911                                    parameters.iter().position(|p| p.name == name_str)
1912                                {
1913                                    let param = &parameters[param_index];
1914                                    let is_provided = param_index < call_args.len()
1915                                        || call_keywords.iter().any(|kw| {
1916                                            kw.arg
1917                                                .as_ref()
1918                                                .is_some_and(|arg| arg.as_str() == name_str)
1919                                        });
1920
1921                                    if !is_provided {
1922                                        // Parameter not provided - only skip if it has defaults
1923                                        if param.has_default {
1924                                            // This is an unsubstituted parameter with default, skip it
1925                                            return None;
1926                                        } else {
1927                                            // This is an unsubstituted parameter without default, keep it
1928                                            // (This handles required parameters like 'self' in methods)
1929                                        }
1930                                    }
1931                                }
1932                            }
1933                        }
1934
1935                        // Skip parameters that were marked for omission
1936                        if value == "__OMIT_PARAMETER__" {
1937                            return None;
1938                        }
1939
1940                        Some(value)
1941                    })
1942                    .collect();
1943                let keywords: Vec<String> = call
1944                    .keywords
1945                    .iter()
1946                    .filter_map(|kw| {
1947                        let value = self.walk_replacement_ast_impl(
1948                            &kw.value,
1949                            call_args,
1950                            call_keywords,
1951                            parameters,
1952                            source_module,
1953                        );
1954                        if let Some(arg) = &kw.arg {
1955                            // Skip parameters that were marked for omission
1956                            if value == "__OMIT_PARAMETER__" {
1957                                return None;
1958                            }
1959
1960                            // Check if this keyword argument's value is an unsubstituted parameter
1961                            let is_unsubstituted = value == arg.as_str()
1962                                || (source_module.is_some()
1963                                    && value
1964                                        == format!("{}.{}", source_module.unwrap(), arg.as_str()));
1965
1966                            if is_unsubstituted {
1967                                // Check if this parameter was actually provided in the original call
1968                                if let Some(param_idx) =
1969                                    parameters.iter().position(|p| p.name == arg.as_str())
1970                                {
1971                                    let param = &parameters[param_idx];
1972                                    let is_provided = param_idx < call_args.len()
1973                                        || call_keywords.iter().any(|kw| {
1974                                            kw.arg
1975                                                .as_ref()
1976                                                .is_some_and(|a| a.as_str() == arg.as_str())
1977                                        });
1978
1979                                    if is_provided {
1980                                        // Parameter was provided, keep it as-is
1981                                        return Some(format!("{}={}", arg, value));
1982                                    } else if param.has_default {
1983                                        // Parameter has default and wasn't provided, omit it
1984                                        return None;
1985                                    } else {
1986                                        // Parameter doesn't have default and wasn't provided - keep it as-is
1987                                        // (This handles required parameters like 'self' in methods)
1988                                        return Some(format!("{}={}", arg, value));
1989                                    }
1990                                }
1991                                // Unknown parameter, keep original behavior (skip it)
1992                                return None;
1993                            }
1994                            Some(format!("{}={}", arg, value))
1995                        } else {
1996                            // This is a starred expression like **kwargs
1997
1998                            // If the value is empty (no kwargs to expand), skip it
1999                            if value.is_empty() {
2000                                return None;
2001                            }
2002
2003                            // Check if the value is already expanded individual arguments (contains =)
2004                            if value.contains("=") {
2005                                // Value is already expanded (like "a=1, b=2" or just "a=1"), don't add **
2006                                Some(value)
2007                            } else if value.starts_with("**") {
2008                                // Value already has ** prefix, don't add another one
2009                                Some(value)
2010                            } else {
2011                                // Value is a single expression like "dict_var" or "kwargs", add **
2012                                Some(format!("**{}", value))
2013                            }
2014                        }
2015                    })
2016                    .collect();
2017
2018                let mut all_args = args;
2019                all_args.extend(keywords);
2020                format!("{}({})", func, all_args.join(", "))
2021            }
2022
2023            rpy_ast::Expr::Constant(constant) => {
2024                // Handle constants like strings, numbers, etc.
2025                self.format_constant(&constant.value)
2026            }
2027
2028            // For all other expression types, recursively handle them
2029            rpy_ast::Expr::IfExp(if_exp) => {
2030                let body = self.walk_replacement_ast_impl(
2031                    &if_exp.body,
2032                    call_args,
2033                    call_keywords,
2034                    parameters,
2035                    source_module,
2036                );
2037                let test = self.walk_replacement_ast_impl(
2038                    &if_exp.test,
2039                    call_args,
2040                    call_keywords,
2041                    parameters,
2042                    source_module,
2043                );
2044                let orelse = self.walk_replacement_ast_impl(
2045                    &if_exp.orelse,
2046                    call_args,
2047                    call_keywords,
2048                    parameters,
2049                    source_module,
2050                );
2051                format!("{} if {} else {}", body, test, orelse)
2052            }
2053
2054            rpy_ast::Expr::BoolOp(boolop) => {
2055                let values: Vec<String> = boolop
2056                    .values
2057                    .iter()
2058                    .map(|v| {
2059                        self.walk_replacement_ast_impl(
2060                            v,
2061                            call_args,
2062                            call_keywords,
2063                            parameters,
2064                            source_module,
2065                        )
2066                    })
2067                    .collect();
2068                let op = match &boolop.op {
2069                    rustpython_ast::BoolOp::And => " and ",
2070                    rustpython_ast::BoolOp::Or => " or ",
2071                };
2072                values.join(op)
2073            }
2074
2075            rpy_ast::Expr::BinOp(binop) => {
2076                let left = self.walk_replacement_ast_impl(
2077                    &binop.left,
2078                    call_args,
2079                    call_keywords,
2080                    parameters,
2081                    source_module,
2082                );
2083                let right = self.walk_replacement_ast_impl(
2084                    &binop.right,
2085                    call_args,
2086                    call_keywords,
2087                    parameters,
2088                    source_module,
2089                );
2090                let op = match &binop.op {
2091                    rustpython_ast::Operator::Add => "+",
2092                    rustpython_ast::Operator::Sub => "-",
2093                    rustpython_ast::Operator::Mult => "*",
2094                    rustpython_ast::Operator::Div => "/",
2095                    rustpython_ast::Operator::Mod => "%",
2096                    rustpython_ast::Operator::Pow => "**",
2097                    rustpython_ast::Operator::FloorDiv => "//",
2098                    rustpython_ast::Operator::LShift => "<<",
2099                    rustpython_ast::Operator::RShift => ">>",
2100                    rustpython_ast::Operator::BitOr => "|",
2101                    rustpython_ast::Operator::BitXor => "^",
2102                    rustpython_ast::Operator::BitAnd => "&",
2103                    rustpython_ast::Operator::MatMult => "@",
2104                };
2105                format!("{} {} {}", left, op, right)
2106            }
2107
2108            rpy_ast::Expr::Subscript(sub) => {
2109                let value = self.walk_replacement_ast_impl(
2110                    &sub.value,
2111                    call_args,
2112                    call_keywords,
2113                    parameters,
2114                    source_module,
2115                );
2116                let slice = self.walk_replacement_ast_impl(
2117                    &sub.slice,
2118                    call_args,
2119                    call_keywords,
2120                    parameters,
2121                    source_module,
2122                );
2123                format!("{}[{}]", value, slice)
2124            }
2125
2126            rpy_ast::Expr::Compare(compare) => {
2127                let mut result = self.walk_replacement_ast_impl(
2128                    &compare.left,
2129                    call_args,
2130                    call_keywords,
2131                    parameters,
2132                    source_module,
2133                );
2134                for (op, comparator) in compare.ops.iter().zip(&compare.comparators) {
2135                    let op_str = match op {
2136                        rustpython_ast::CmpOp::Eq => " == ",
2137                        rustpython_ast::CmpOp::NotEq => " != ",
2138                        rustpython_ast::CmpOp::Lt => " < ",
2139                        rustpython_ast::CmpOp::LtE => " <= ",
2140                        rustpython_ast::CmpOp::Gt => " > ",
2141                        rustpython_ast::CmpOp::GtE => " >= ",
2142                        rustpython_ast::CmpOp::Is => " is ",
2143                        rustpython_ast::CmpOp::IsNot => " is not ",
2144                        rustpython_ast::CmpOp::In => " in ",
2145                        rustpython_ast::CmpOp::NotIn => " not in ",
2146                    };
2147                    result.push_str(op_str);
2148                    result.push_str(&self.walk_replacement_ast_impl(
2149                        comparator,
2150                        call_args,
2151                        call_keywords,
2152                        parameters,
2153                        source_module,
2154                    ));
2155                }
2156                result
2157            }
2158
2159            rpy_ast::Expr::UnaryOp(unaryop) => {
2160                let operand = self.walk_replacement_ast_impl(
2161                    &unaryop.operand,
2162                    call_args,
2163                    call_keywords,
2164                    parameters,
2165                    source_module,
2166                );
2167                let op = match &unaryop.op {
2168                    rustpython_ast::UnaryOp::Not => "not ",
2169                    rustpython_ast::UnaryOp::UAdd => "+",
2170                    rustpython_ast::UnaryOp::USub => "-",
2171                    rustpython_ast::UnaryOp::Invert => "~",
2172                };
2173                format!("{}{}", op, operand)
2174            }
2175
2176            rpy_ast::Expr::List(list) => {
2177                let items: Vec<String> = list
2178                    .elts
2179                    .iter()
2180                    .map(|e| {
2181                        self.walk_replacement_ast_impl(
2182                            e,
2183                            call_args,
2184                            call_keywords,
2185                            parameters,
2186                            source_module,
2187                        )
2188                    })
2189                    .collect();
2190                format!("[{}]", items.join(", "))
2191            }
2192
2193            rpy_ast::Expr::Tuple(tuple) => {
2194                let items: Vec<String> = tuple
2195                    .elts
2196                    .iter()
2197                    .map(|e| {
2198                        self.walk_replacement_ast_impl(
2199                            e,
2200                            call_args,
2201                            call_keywords,
2202                            parameters,
2203                            source_module,
2204                        )
2205                    })
2206                    .collect();
2207                if items.len() == 1 {
2208                    format!("({},)", items[0])
2209                } else {
2210                    format!("({})", items.join(", "))
2211                }
2212            }
2213
2214            rpy_ast::Expr::Dict(dict) => {
2215                let mut items = Vec::new();
2216                for (key_opt, value) in dict.keys.iter().zip(&dict.values) {
2217                    if let Some(key) = key_opt {
2218                        let key_str = self.walk_replacement_ast_impl(
2219                            key,
2220                            call_args,
2221                            call_keywords,
2222                            parameters,
2223                            source_module,
2224                        );
2225                        let value_str = self.walk_replacement_ast_impl(
2226                            value,
2227                            call_args,
2228                            call_keywords,
2229                            parameters,
2230                            source_module,
2231                        );
2232                        items.push(format!("{}: {}", key_str, value_str));
2233                    } else {
2234                        // None key means dictionary unpacking
2235                        let value_str = self.walk_replacement_ast_impl(
2236                            value,
2237                            call_args,
2238                            call_keywords,
2239                            parameters,
2240                            source_module,
2241                        );
2242                        items.push(format!("**{}", value_str));
2243                    }
2244                }
2245                format!("{{{}}}", items.join(", "))
2246            }
2247
2248            rpy_ast::Expr::Set(set) => {
2249                let items: Vec<String> = set
2250                    .elts
2251                    .iter()
2252                    .map(|e| {
2253                        self.walk_replacement_ast_impl(
2254                            e,
2255                            call_args,
2256                            call_keywords,
2257                            parameters,
2258                            source_module,
2259                        )
2260                    })
2261                    .collect();
2262                format!("{{{}}}", items.join(", "))
2263            }
2264
2265            rpy_ast::Expr::NamedExpr(named) => {
2266                let target = self.walk_replacement_ast_impl(
2267                    &named.target,
2268                    call_args,
2269                    call_keywords,
2270                    parameters,
2271                    source_module,
2272                );
2273                let value = self.walk_replacement_ast_impl(
2274                    &named.value,
2275                    call_args,
2276                    call_keywords,
2277                    parameters,
2278                    source_module,
2279                );
2280                format!("({} := {})", target, value)
2281            }
2282
2283            rpy_ast::Expr::Starred(starred) => {
2284                // Check if this is *args that needs expansion
2285                if let rpy_ast::Expr::Name(name) = &*starred.value {
2286                    if let Some(_param) = parameters
2287                        .iter()
2288                        .find(|p| p.is_vararg && p.name == name.id.as_str())
2289                    {
2290                        // This is *args - expand to actual arguments
2291                        let regular_param_count = parameters
2292                            .iter()
2293                            .filter(|p| !p.is_vararg && !p.is_kwarg)
2294                            .count();
2295
2296                        // For functions with only *args and **kwargs, all positional args go to *args
2297                        if regular_param_count == 0 {
2298                            let args_strs: Vec<String> = call_args
2299                                .iter()
2300                                .map(|arg| self.expr_to_string(arg))
2301                                .collect();
2302                            return args_strs.join(", ");
2303                        }
2304
2305                        // For functions with some regular params, extra args go to *args
2306                        if call_args.len() > regular_param_count {
2307                            let extra_args: Vec<String> = call_args[regular_param_count..]
2308                                .iter()
2309                                .map(|arg| self.expr_to_string(arg))
2310                                .collect();
2311                            return extra_args.join(", ");
2312                        }
2313                        return String::new(); // No extra args
2314                    }
2315                }
2316
2317                // Not *args - just a regular starred expression
2318                let value = self.walk_replacement_ast_impl(
2319                    &starred.value,
2320                    call_args,
2321                    call_keywords,
2322                    parameters,
2323                    source_module,
2324                );
2325                format!("*{}", value)
2326            }
2327
2328            rpy_ast::Expr::Await(await_expr) => {
2329                let value = self.walk_replacement_ast_impl(
2330                    &await_expr.value,
2331                    call_args,
2332                    call_keywords,
2333                    parameters,
2334                    source_module,
2335                );
2336                format!("await {}", value)
2337            }
2338
2339            rpy_ast::Expr::Slice(slice) => {
2340                let lower = slice
2341                    .lower
2342                    .as_ref()
2343                    .map(|e| {
2344                        self.walk_replacement_ast_impl(
2345                            e,
2346                            call_args,
2347                            call_keywords,
2348                            parameters,
2349                            source_module,
2350                        )
2351                    })
2352                    .unwrap_or_default();
2353                let upper = slice
2354                    .upper
2355                    .as_ref()
2356                    .map(|e| {
2357                        self.walk_replacement_ast_impl(
2358                            e,
2359                            call_args,
2360                            call_keywords,
2361                            parameters,
2362                            source_module,
2363                        )
2364                    })
2365                    .unwrap_or_default();
2366                if let Some(step) = &slice.step {
2367                    let step_str = self.walk_replacement_ast_impl(
2368                        step,
2369                        call_args,
2370                        call_keywords,
2371                        parameters,
2372                        source_module,
2373                    );
2374                    format!("{}:{}:{}", lower, upper, step_str)
2375                } else {
2376                    format!("{}:{}", lower, upper)
2377                }
2378            }
2379
2380            // For other complex expression types that don't need parameter substitution,
2381            // use the regular expr_to_string_with_placeholders
2382            _ => {
2383                // For now, use the comprehensive handler for other types
2384                self.expr_to_string_with_placeholders(expr, &HashSet::new())
2385            }
2386        }
2387    }
2388
2389    #[allow(clippy::only_used_in_recursion)] // self is needed for recursive method calls
2390    fn format_constant(&self, constant: &rustpython_ast::Constant) -> String {
2391        use rustpython_ast::Constant;
2392        match constant {
2393            Constant::Str(s) => format!("\"{}\"", s.replace("\\", "\\\\").replace("\"", "\\\"")),
2394            Constant::Bytes(b) => format!("b\"{}\"", String::from_utf8_lossy(b)),
2395            Constant::Int(i) => i.to_string(),
2396            Constant::Float(f) => f.to_string(),
2397            Constant::Complex { real, imag } => {
2398                if *real == 0.0 {
2399                    format!("{}j", imag)
2400                } else if *imag == 0.0 {
2401                    format!("{}", real)
2402                } else if *imag < 0.0 {
2403                    format!("{}{}j", real, imag)
2404                } else {
2405                    format!("{}+{}j", real, imag)
2406                }
2407            }
2408            Constant::Bool(b) => {
2409                if *b {
2410                    "True".to_string()
2411                } else {
2412                    "False".to_string()
2413                }
2414            }
2415            Constant::None => "None".to_string(),
2416            Constant::Ellipsis => "...".to_string(),
2417            Constant::Tuple(items) => {
2418                let elements: Vec<String> = items
2419                    .iter()
2420                    .map(|item| self.format_constant(item))
2421                    .collect();
2422                format!("({})", elements.join(", "))
2423            }
2424        }
2425    }
2426
2427    fn rustpython_expr_to_string_for_template(
2428        &self,
2429        expr: &rustpython_ast::Expr,
2430        param_map: &HashMap<String, &ast::Expr>,
2431        all_args: &[ast::Expr],
2432        all_keywords: &[ast::Keyword],
2433        parameters: &[ParameterInfo],
2434    ) -> String {
2435        self.rustpython_expr_to_string_with_args_impl(
2436            expr,
2437            param_map,
2438            all_args,
2439            all_keywords,
2440            parameters,
2441            false,
2442        )
2443    }
2444
2445    fn rustpython_expr_to_string_with_args_impl(
2446        &self,
2447        expr: &rustpython_ast::Expr,
2448        param_map: &HashMap<String, &ast::Expr>,
2449        all_args: &[ast::Expr],
2450        all_keywords: &[ast::Keyword],
2451        parameters: &[ParameterInfo],
2452        apply_name_resolution: bool,
2453    ) -> String {
2454        use rustpython_ast as rpy_ast;
2455
2456        match expr {
2457            rpy_ast::Expr::Name(name) => {
2458                let name_str = name.id.as_str();
2459
2460                // Check if this is a parameter that should be substituted
2461                if let Some(arg_expr) = param_map.get(name_str) {
2462                    return self.expr_to_string(arg_expr);
2463                }
2464
2465                // Check if this is *args - don't substitute, let Starred handle it
2466                if parameters.iter().any(|p| p.is_vararg && p.name == name_str) {
2467                    return name_str.to_string();
2468                }
2469
2470                // Check if this is **kwargs - don't substitute, let the dictionary unpacking handle it
2471                if parameters.iter().any(|p| p.is_kwarg && p.name == name_str) {
2472                    return name_str.to_string();
2473                }
2474
2475                // This name doesn't have a substitution value
2476                // Check if it's supposed to be a parameter that should have been provided
2477                // Only return empty if we're actively doing substitution (param_map has entries)
2478                // and this is a regular parameter that should have been mapped
2479                if !param_map.is_empty()
2480                    && parameters
2481                        .iter()
2482                        .any(|p| p.name == name_str && !p.is_vararg && !p.is_kwarg)
2483                {
2484                    // We're doing parameter substitution and this parameter wasn't provided
2485                    return String::new();
2486                }
2487
2488                // Otherwise, return the name as-is
2489                name_str.to_string()
2490            }
2491            rpy_ast::Expr::Call(call) => {
2492                // Handle function calls in the replacement - need to check if function needs module prefix
2493                let func_str = match &*call.func {
2494                    rpy_ast::Expr::Name(name) => {
2495                        let func_name = name.id.as_str();
2496                        // First check if this is a parameter that should be substituted
2497                        if let Some(param_expr) = param_map.get(func_name) {
2498                            self.expr_to_string(param_expr)
2499                        } else if let Some(full_name) = self.import_map.get(func_name) {
2500                            // Check if this function name is imported
2501                            full_name.clone()
2502                        } else {
2503                            // Check if it's a builtin function
2504                            if apply_name_resolution && !self.builtins.contains(func_name) {
2505                                // Not imported and not a builtin, add module prefix
2506                                format!("{}.{}", self.module_name, func_name)
2507                            } else {
2508                                func_name.to_string()
2509                            }
2510                        }
2511                    }
2512                    _ => {
2513                        // For attribute calls or other complex expressions, use normal processing
2514                        self.rustpython_expr_to_string_with_args_impl(
2515                            &call.func,
2516                            param_map,
2517                            all_args,
2518                            all_keywords,
2519                            parameters,
2520                            apply_name_resolution,
2521                        )
2522                    }
2523                };
2524
2525                let mut args = Vec::new();
2526                for arg in &call.args {
2527                    let arg_str = self.rustpython_expr_to_string_with_args(
2528                        arg,
2529                        param_map,
2530                        all_args,
2531                        all_keywords,
2532                        parameters,
2533                    );
2534                    if !arg_str.is_empty() {
2535                        args.push(arg_str);
2536                    }
2537                }
2538
2539                // Handle keywords in the replacement (including **kwargs expansion)
2540                for keyword in &call.keywords {
2541                    if let Some(arg_name) = &keyword.arg {
2542                        let value = self.rustpython_expr_to_string_with_args(
2543                            &keyword.value,
2544                            param_map,
2545                            all_args,
2546                            all_keywords,
2547                            parameters,
2548                        );
2549                        // Skip if the value is just the parameter name (meaning it wasn't provided)
2550                        if !value.is_empty() && value != arg_name.as_str() {
2551                            args.push(format!("{}={}", arg_name, value));
2552                        }
2553                    } else {
2554                        // This is **something - check if it's **kwargs placeholder
2555                        if let rpy_ast::Expr::Name(name) = &keyword.value {
2556                            if parameters
2557                                .iter()
2558                                .any(|p| p.is_kwarg && p.name == name.id.as_str())
2559                            {
2560                                // This is **kwargs - check if we have actual dict unpacking
2561                                let has_dict_unpack =
2562                                    all_keywords.iter().any(|kw| kw.arg.is_none());
2563
2564                                if has_dict_unpack {
2565                                    // We have **dict unpacking in the actual call - preserve it
2566                                    for kw in all_keywords {
2567                                        if kw.arg.is_none() {
2568                                            // This is dict unpacking
2569                                            args.push(format!(
2570                                                "**{}",
2571                                                self.expr_to_string(&kw.value)
2572                                            ));
2573                                        } else {
2574                                            // Regular keyword argument
2575                                            args.push(format!(
2576                                                "{}={}",
2577                                                kw.arg.as_ref().unwrap(),
2578                                                self.expr_to_string(&kw.value)
2579                                            ));
2580                                        }
2581                                    }
2582                                } else {
2583                                    // No dict unpacking, just regular keyword args
2584                                    for kw in all_keywords {
2585                                        if let Some(arg_name) = &kw.arg {
2586                                            args.push(format!(
2587                                                "{}={}",
2588                                                arg_name,
2589                                                self.expr_to_string(&kw.value)
2590                                            ));
2591                                        }
2592                                    }
2593                                }
2594                                continue;
2595                            }
2596                        }
2597                        // Regular ** unpacking
2598                        let value = self.rustpython_expr_to_string_with_args(
2599                            &keyword.value,
2600                            param_map,
2601                            all_args,
2602                            all_keywords,
2603                            parameters,
2604                        );
2605                        if !value.is_empty() {
2606                            args.push(format!("**{}", value));
2607                        }
2608                    }
2609                }
2610
2611                // Check if we need to add preserved dict unpacking
2612                let has_dict_unpack = all_keywords.iter().any(|kw| kw.arg.is_none());
2613                let has_kwarg_param = parameters.iter().any(|p| p.is_kwarg);
2614
2615                if has_dict_unpack && !has_kwarg_param {
2616                    // Add the preserved dict unpacking
2617                    for kw in all_keywords {
2618                        if kw.arg.is_none() {
2619                            args.push(format!("**{}", self.expr_to_string(&kw.value)));
2620                        }
2621                    }
2622                }
2623
2624                format!("{}({})", func_str, args.join(", "))
2625            }
2626            rpy_ast::Expr::Attribute(attr) => {
2627                let value = self.rustpython_expr_to_string_with_args(
2628                    &attr.value,
2629                    param_map,
2630                    all_args,
2631                    all_keywords,
2632                    parameters,
2633                );
2634                format!("{}.{}", value, attr.attr)
2635            }
2636            rpy_ast::Expr::BinOp(binop) => {
2637                let left = self.rustpython_expr_to_string_with_args(
2638                    &binop.left,
2639                    param_map,
2640                    all_args,
2641                    all_keywords,
2642                    parameters,
2643                );
2644                let right = self.rustpython_expr_to_string_with_args(
2645                    &binop.right,
2646                    param_map,
2647                    all_args,
2648                    all_keywords,
2649                    parameters,
2650                );
2651                let op = match &binop.op {
2652                    rpy_ast::Operator::Add => "+",
2653                    rpy_ast::Operator::Sub => "-",
2654                    rpy_ast::Operator::Mult => "*",
2655                    rpy_ast::Operator::Div => "/",
2656                    rpy_ast::Operator::FloorDiv => "//",
2657                    rpy_ast::Operator::Mod => "%",
2658                    rpy_ast::Operator::Pow => "**",
2659                    rpy_ast::Operator::LShift => "<<",
2660                    rpy_ast::Operator::RShift => ">>",
2661                    rpy_ast::Operator::BitOr => "|",
2662                    rpy_ast::Operator::BitXor => "^",
2663                    rpy_ast::Operator::BitAnd => "&",
2664                    rpy_ast::Operator::MatMult => "@",
2665                };
2666                format!("{} {} {}", left, op, right)
2667            }
2668            rpy_ast::Expr::Starred(starred) => {
2669                // Check if this is *args
2670                if let rpy_ast::Expr::Name(name) = &*starred.value {
2671                    if let Some(_param) = parameters
2672                        .iter()
2673                        .find(|p| p.is_vararg && p.name == name.id.as_str())
2674                    {
2675                        // This is *args - expand to actual arguments
2676                        let regular_param_count = parameters
2677                            .iter()
2678                            .filter(|p| !p.is_vararg && !p.is_kwarg)
2679                            .count();
2680
2681                        // For functions with only *args and **kwargs, all positional args go to *args
2682                        if regular_param_count == 0 {
2683                            let args_strs: Vec<String> = all_args
2684                                .iter()
2685                                .map(|arg| self.expr_to_string(arg))
2686                                .collect();
2687                            return args_strs.join(", ");
2688                        }
2689
2690                        // For functions with some regular params, extra args go to *args
2691                        if all_args.len() > regular_param_count {
2692                            let extra_args: Vec<String> = all_args[regular_param_count..]
2693                                .iter()
2694                                .map(|arg| self.expr_to_string(arg))
2695                                .collect();
2696                            return extra_args.join(", ");
2697                        }
2698                        return String::new(); // No extra args
2699                    }
2700                }
2701
2702                // Regular starred expression
2703                let value = self.rustpython_expr_to_string_with_args(
2704                    &starred.value,
2705                    param_map,
2706                    all_args,
2707                    all_keywords,
2708                    parameters,
2709                );
2710                format!("*{}", value)
2711            }
2712            // Handle dictionary unpacking for **kwargs
2713            rpy_ast::Expr::Dict(dict) if dict.keys.is_empty() && dict.values.len() == 1 => {
2714                // This might be **kwargs
2715                if let rpy_ast::Expr::Name(name) = &dict.values[0] {
2716                    if let Some(_param) = parameters
2717                        .iter()
2718                        .find(|p| p.is_kwarg && p.name == name.id.as_str())
2719                    {
2720                        // This is **kwargs - expand to actual keyword arguments
2721                        let kwargs: Vec<String> = all_keywords
2722                            .iter()
2723                            .map(|kw| {
2724                                if let Some(arg_name) = &kw.arg {
2725                                    format!("{}={}", arg_name, self.expr_to_string(&kw.value))
2726                                } else {
2727                                    format!("**{}", self.expr_to_string(&kw.value))
2728                                }
2729                            })
2730                            .collect();
2731                        return kwargs.join(", ");
2732                    }
2733                }
2734                // Fall through to default handling
2735                let param_names: HashSet<String> = param_map.keys().cloned().collect();
2736                self.expr_to_string_with_placeholders(expr, &param_names)
2737            }
2738            rpy_ast::Expr::Await(await_expr) => {
2739                // Handle await expressions by recursively processing the value
2740                let value = self.rustpython_expr_to_string_with_args(
2741                    &await_expr.value,
2742                    param_map,
2743                    all_args,
2744                    all_keywords,
2745                    parameters,
2746                );
2747                format!("await {}", value)
2748            }
2749            rpy_ast::Expr::Subscript(subscript) => {
2750                // Handle subscript expressions like dict[key]
2751                let value = self.rustpython_expr_to_string_with_args(
2752                    &subscript.value,
2753                    param_map,
2754                    all_args,
2755                    all_keywords,
2756                    parameters,
2757                );
2758                let slice = self.rustpython_expr_to_string_with_args(
2759                    &subscript.slice,
2760                    param_map,
2761                    all_args,
2762                    all_keywords,
2763                    parameters,
2764                );
2765                format!("{}[{}]", value, slice)
2766            }
2767            rpy_ast::Expr::IfExp(if_exp) => {
2768                // Handle conditional expressions like (x if condition else y)
2769                let body = self.rustpython_expr_to_string_with_args(
2770                    &if_exp.body,
2771                    param_map,
2772                    all_args,
2773                    all_keywords,
2774                    parameters,
2775                );
2776                let test = self.rustpython_expr_to_string_with_args(
2777                    &if_exp.test,
2778                    param_map,
2779                    all_args,
2780                    all_keywords,
2781                    parameters,
2782                );
2783                let orelse = self.rustpython_expr_to_string_with_args(
2784                    &if_exp.orelse,
2785                    param_map,
2786                    all_args,
2787                    all_keywords,
2788                    parameters,
2789                );
2790                format!("{} if {} else {}", body, test, orelse)
2791            }
2792            rpy_ast::Expr::UnaryOp(unary) => {
2793                // Handle unary operations like -x, not x
2794                let operand = self.rustpython_expr_to_string_with_args(
2795                    &unary.operand,
2796                    param_map,
2797                    all_args,
2798                    all_keywords,
2799                    parameters,
2800                );
2801                let op = match &unary.op {
2802                    rustpython_ast::UnaryOp::Not => "not ",
2803                    rustpython_ast::UnaryOp::UAdd => "+",
2804                    rustpython_ast::UnaryOp::USub => "-",
2805                    rustpython_ast::UnaryOp::Invert => "~",
2806                };
2807                format!("{}{}", op, operand)
2808            }
2809            rpy_ast::Expr::Compare(compare) => {
2810                // Handle comparison operations like x > 0
2811                let mut result = self.rustpython_expr_to_string_with_args(
2812                    &compare.left,
2813                    param_map,
2814                    all_args,
2815                    all_keywords,
2816                    parameters,
2817                );
2818                for (op, comparator) in compare.ops.iter().zip(&compare.comparators) {
2819                    let op_str = match op {
2820                        rustpython_ast::CmpOp::Eq => " == ",
2821                        rustpython_ast::CmpOp::NotEq => " != ",
2822                        rustpython_ast::CmpOp::Lt => " < ",
2823                        rustpython_ast::CmpOp::LtE => " <= ",
2824                        rustpython_ast::CmpOp::Gt => " > ",
2825                        rustpython_ast::CmpOp::GtE => " >= ",
2826                        rustpython_ast::CmpOp::Is => " is ",
2827                        rustpython_ast::CmpOp::IsNot => " is not ",
2828                        rustpython_ast::CmpOp::In => " in ",
2829                        rustpython_ast::CmpOp::NotIn => " not in ",
2830                    };
2831                    result.push_str(op_str);
2832                    result.push_str(&self.rustpython_expr_to_string_with_args(
2833                        comparator,
2834                        param_map,
2835                        all_args,
2836                        all_keywords,
2837                        parameters,
2838                    ));
2839                }
2840                result
2841            }
2842            rpy_ast::Expr::NamedExpr(named) => {
2843                // Handle walrus operator (x := value)
2844                let target = self.rustpython_expr_to_string_with_args(
2845                    &named.target,
2846                    param_map,
2847                    all_args,
2848                    all_keywords,
2849                    parameters,
2850                );
2851                let value = self.rustpython_expr_to_string_with_args(
2852                    &named.value,
2853                    param_map,
2854                    all_args,
2855                    all_keywords,
2856                    parameters,
2857                );
2858                format!("({} := {})", target, value)
2859            }
2860            rpy_ast::Expr::BoolOp(boolop) => {
2861                // Handle boolean operations like (a or b)
2862                let values: Vec<String> = boolop
2863                    .values
2864                    .iter()
2865                    .map(|v| {
2866                        self.rustpython_expr_to_string_with_args(
2867                            v,
2868                            param_map,
2869                            all_args,
2870                            all_keywords,
2871                            parameters,
2872                        )
2873                    })
2874                    .collect();
2875                let op = match &boolop.op {
2876                    rustpython_ast::BoolOp::And => " and ",
2877                    rustpython_ast::BoolOp::Or => " or ",
2878                };
2879                values.join(op)
2880            }
2881            rpy_ast::Expr::Dict(dict) => {
2882                // Handle dictionary literals like {}
2883                let mut items = Vec::new();
2884                for (key_opt, value) in dict.keys.iter().zip(&dict.values) {
2885                    if let Some(key) = key_opt {
2886                        let key_str = self.rustpython_expr_to_string_with_args(
2887                            key,
2888                            param_map,
2889                            all_args,
2890                            all_keywords,
2891                            parameters,
2892                        );
2893                        let value_str = self.rustpython_expr_to_string_with_args(
2894                            value,
2895                            param_map,
2896                            all_args,
2897                            all_keywords,
2898                            parameters,
2899                        );
2900                        items.push(format!("{}: {}", key_str, value_str));
2901                    } else {
2902                        // None key means dictionary unpacking
2903                        let value_str = self.rustpython_expr_to_string_with_args(
2904                            value,
2905                            param_map,
2906                            all_args,
2907                            all_keywords,
2908                            parameters,
2909                        );
2910                        items.push(format!("**{}", value_str));
2911                    }
2912                }
2913                format!("{{{}}}", items.join(", "))
2914            }
2915            // For other expression types, convert using the existing method
2916            _ => {
2917                // Check if this is a **kwargs reference in a Call's keywords
2918                if let rpy_ast::Expr::Name(name) = expr {
2919                    if let Some(_param) = parameters
2920                        .iter()
2921                        .find(|p| p.is_kwarg && p.name == name.id.as_str())
2922                    {
2923                        // This is **kwargs in a keyword context - expand it
2924                        let kwargs: Vec<String> = all_keywords
2925                            .iter()
2926                            .map(|kw| {
2927                                if let Some(arg_name) = &kw.arg {
2928                                    format!("{}={}", arg_name, self.expr_to_string(&kw.value))
2929                                } else {
2930                                    format!("**{}", self.expr_to_string(&kw.value))
2931                                }
2932                            })
2933                            .collect();
2934                        if !kwargs.is_empty() {
2935                            return kwargs.join(", ");
2936                        }
2937                    }
2938                }
2939
2940                // Use the existing expr_to_string_with_placeholders but with a custom param_names set
2941                let param_names: HashSet<String> = param_map.keys().cloned().collect();
2942                self.expr_to_string_with_placeholders(expr, &param_names)
2943            }
2944        }
2945    }
2946
2947    /// Transform a replacement AST with parameter substitution
2948    /// Determine if a magic method call should be unwrapped based on AST structure
2949    fn should_unwrap_magic_method_call(
2950        &self,
2951        replacement_ast: &rustpython_ast::Expr,
2952        builtin_name: &str,
2953    ) -> bool {
2954        use rustpython_ast::Expr;
2955
2956        tracing::debug!(
2957            "should_unwrap_magic_method_call: builtin_name={}",
2958            builtin_name
2959        );
2960
2961        // Check if this is a call to the same builtin function
2962        if let Expr::Call(call) = replacement_ast {
2963            if let Expr::Name(func_name) = &*call.func {
2964                if func_name.id.as_str() == builtin_name && call.args.len() == 1 {
2965                    tracing::debug!("Found matching builtin call, checking argument type");
2966                    // This is builtin(something) - check what the something is
2967                    match &call.args[0] {
2968                        // Simple parameter reference like {self} -> unwrap only if it's a direct parameter
2969                        Expr::Name(name) => {
2970                            let should_unwrap = name.id.as_str() == "self"; // Only unwrap if it's literally 'self'
2971                            tracing::debug!(
2972                                "Name argument '{}', should_unwrap: {}",
2973                                name.id,
2974                                should_unwrap
2975                            );
2976                            should_unwrap
2977                        }
2978
2979                        // Attribute access like {self}.value -> DON'T unwrap, keep the builtin call
2980                        Expr::Attribute(_) => {
2981                            tracing::debug!(
2982                                "Attribute argument - NOT unwrapping to preserve builtin call"
2983                            );
2984                            false
2985                        }
2986
2987                        // Call expression like {self}.method() -> unwrap if it's a method call on self
2988                        Expr::Call(call_expr) => {
2989                            // Check if this is a method call on self (self.method())
2990                            if let Expr::Attribute(attr) = &*call_expr.func {
2991                                if let Expr::Name(base_name) = &*attr.value {
2992                                    if base_name.id.as_str() == "self" {
2993                                        // This is self.method() - unwrap since the method should return the right type
2994                                        tracing::debug!("Method call on self - unwrapping");
2995                                        return true;
2996                                    }
2997                                }
2998                            }
2999                            tracing::debug!("Call argument - NOT unwrapping");
3000                            false
3001                        }
3002
3003                        // Other complex expressions -> don't unwrap
3004                        _ => {
3005                            tracing::debug!("Other expression type - NOT unwrapping");
3006                            false
3007                        }
3008                    }
3009                } else {
3010                    tracing::debug!("Not matching builtin call or wrong arg count");
3011                    false
3012                }
3013            } else {
3014                tracing::debug!("Call func is not a simple name");
3015                false
3016            }
3017        } else {
3018            tracing::debug!("Replacement is not a call expression");
3019            false
3020        }
3021    }
3022}